├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── logo.png ├── package-lock.json ├── package.json ├── projects ├── angular2-promise-buttons-demo │ ├── e2e │ │ ├── protractor.conf.js │ │ └── tsconfig.json │ ├── karma.conf.js │ ├── src │ │ ├── app │ │ │ ├── app.component.css │ │ │ ├── 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 │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json └── angular2-promise-buttons │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── angular2-promise-buttons.module.ts │ ├── default-promise-btn-config.ts │ ├── index.ts │ ├── promise-btn-config.ts │ ├── promise-btn.directive.spec.ts │ ├── promise-btn.directive.ts │ ├── test.ts │ └── user-cfg.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tslint.json │ └── yarn.lock ├── scripts └── copy-readme-to-demo.js ├── tsconfig.build-lib.json ├── tsconfig.json ├── wallaby.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://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/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [johannesjo] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | 5 | cache: 6 | yarn: true 7 | 8 | 9 | script: 10 | - yarn 11 | - yarn test-coverage 12 | 13 | addons: 14 | apt: 15 | sources: 16 | - ubuntu-toolchain-r-test 17 | # required by node-gyp to build some packages 18 | packages: 19 | - g++-4.8 20 | 21 | after_success: 'npm run coveralls' 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [4.0.1](https://github.com/johannesjo/angular2-promise-buttons/compare/v4.0.0...v4.0.1) (2019-11-18) 3 | 4 | 5 | 6 | 7 | # [4.0.0](https://github.com/johannesjo/angular2-promise-buttons/compare/v3.0.0...v4.0.0) (2019-06-08) 8 | 9 | 10 | ### Bug Fixes 11 | 12 | * typing error ([a26ebec](https://github.com/johannesjo/angular2-promise-buttons/commit/a26ebec)) 13 | 14 | 15 | ### Features 16 | 17 | * support regular booleans ([be350c1](https://github.com/johannesjo/angular2-promise-buttons/commit/be350c1)) 18 | 19 | 20 | 21 | 22 | ## [3.0.1](https://github.com/johannesjo/angular2-promise-buttons/compare/v3.0.0...v3.0.1) (2019-01-20) 23 | 24 | 25 | ### Features 26 | 27 | * support regular booleans ([be350c1](https://github.com/johannesjo/angular2-promise-buttons/commit/be350c1)) 28 | 29 | 30 | 31 | 32 | # [3.0.0](https://github.com/johannesjo/angular2-promise-buttons/compare/v2.1.1...v3.0.0) (2018-11-11) 33 | 34 | 35 | 36 | 37 | ## [2.1.1](https://github.com/johannesjo/angular2-promise-buttons/compare/v2.1.0...v2.1.1) (2018-05-19) 38 | 39 | 40 | 41 | 42 | # [2.1.0](https://github.com/johannesjo/angular2-promise-buttons/compare/v2.0.1...v2.1.0) (2018-05-12) 43 | 44 | 45 | ### Features 46 | 47 | * update dist files [#22](https://github.com/johannesjo/angular2-promise-buttons/issues/22) ([46d7e1c](https://github.com/johannesjo/angular2-promise-buttons/commit/46d7e1c)) 48 | * update to angular 6 [#22](https://github.com/johannesjo/angular2-promise-buttons/issues/22) ([94f25e2](https://github.com/johannesjo/angular2-promise-buttons/commit/94f25e2)) 49 | 50 | 51 | 52 | 53 | ## [2.0.1](https://github.com/johannesjo/angular2-promise-buttons/compare/v2.0.0...v2.0.1) (2017-11-15) 54 | 55 | 56 | 57 | 58 | # [2.0.0](https://github.com/johannesjo/angular2-promise-buttons/compare/v1.1.0...v2.0.0) (2017-11-03) 59 | 60 | 61 | ### Features 62 | 63 | * move to angular5 ([562406b](https://github.com/johannesjo/angular2-promise-buttons/commit/562406b)) 64 | 65 | 66 | 67 | 68 | # [1.1.0](https://github.com/johannesjo/angular2-promise-buttons/compare/v1.0.13...v1.1.0) (2017-10-27) 69 | 70 | 71 | 72 | 73 | ## [1.0.13](https://github.com/johannesjo/angular2-promise-buttons/compare/v1.0.12...v1.0.13) (2017-07-19) 74 | 75 | 76 | 77 | 78 | ## [1.0.12](https://github.com/johannesjo/angular2-promise-buttons/compare/v1.0.11...v1.0.12) (2017-07-19) 79 | 80 | 81 | ### Features 82 | 83 | * remove bluebird dependency ([9798bd0](https://github.com/johannesjo/angular2-promise-buttons/commit/9798bd0)) 84 | 85 | 86 | 87 | 88 | ## [1.0.11](https://github.com/johannesjo/angular2-promise-buttons/compare/v1.0.10...v1.0.11) (2017-07-18) 89 | 90 | 91 | 92 | 93 | ## [1.0.10](https://github.com/johannesjo/angular2-promise-buttons/compare/v1.0.9...v1.0.10) (2017-07-17) 94 | 95 | 96 | 97 | 98 | ## [1.0.9](https://github.com/johannesjo/angular2-promise-buttons/compare/v1.0.7...v1.0.9) (2017-07-17) 99 | 100 | 101 | ### Bug Fixes 102 | 103 | * add missing changelog ([0408a91](https://github.com/johannesjo/angular2-promise-buttons/commit/0408a91)) 104 | * weird issue making promiseBtn break inside form if handleCurrentBtnOnly is true by adding a timeout [#7](https://github.com/johannesjo/angular2-promise-buttons/issues/7) ([67b9870](https://github.com/johannesjo/angular2-promise-buttons/commit/67b9870)) 105 | 106 | 107 | ### Features 108 | 109 | * add example for an observable ([54965a0](https://github.com/johannesjo/angular2-promise-buttons/commit/54965a0)) 110 | * beautify demo page ([1205fee](https://github.com/johannesjo/angular2-promise-buttons/commit/1205fee)) 111 | * beautify demo page some more ([27e87f0](https://github.com/johannesjo/angular2-promise-buttons/commit/27e87f0)) 112 | 113 | 114 | 115 | 116 | ## [1.0.8](https://github.com/johannesjo/angular2-promise-buttons/compare/v1.0.7...v1.0.8) (2017-07-17) 117 | 118 | 119 | ### Bug Fixes 120 | 121 | * add missing changelog ([0408a91](https://github.com/johannesjo/angular2-promise-buttons/commit/0408a91)) 122 | * weird issue making promiseBtn break inside form if handleCurrentBtnOnly is true by adding a timeout [#7](https://github.com/johannesjo/angular2-promise-buttons/issues/7) ([67b9870](https://github.com/johannesjo/angular2-promise-buttons/commit/67b9870)) 123 | 124 | 125 | ### Features 126 | 127 | * add example for an observable ([54965a0](https://github.com/johannesjo/angular2-promise-buttons/commit/54965a0)) 128 | * beautify demo page ([1205fee](https://github.com/johannesjo/angular2-promise-buttons/commit/1205fee)) 129 | * beautify demo page some more ([27e87f0](https://github.com/johannesjo/angular2-promise-buttons/commit/27e87f0)) 130 | 131 | 132 | 133 | 134 | ## [1.0.8](https://github.com/johannesjo/angular2-promise-buttons/compare/v1.0.7...v1.0.8) (2017-06-02) 135 | 136 | 137 | ### Bug Fixes 138 | 139 | * weird issue making promiseBtn break inside form if handleCurrentBtnOnly is true by adding a timeout [#7](https://github.com/johannesjo/angular2-promise-buttons/issues/7) ([67b9870](https://github.com/johannesjo/angular2-promise-buttons/commit/67b9870)) 140 | 141 | 142 | ### Features 143 | 144 | * beautify demo page ([1205fee](https://github.com/johannesjo/angular2-promise-buttons/commit/1205fee)) 145 | * beautify demo page some more ([27e87f0](https://github.com/johannesjo/angular2-promise-buttons/commit/27e87f0)) 146 | 147 | 148 | 149 | 150 | ## [1.0.7](https://github.com/johannesjo/angular2-promise-buttons/compare/v1.0.6...v1.0.7) (2017-05-11) 151 | 152 | 153 | ### Features 154 | 155 | * add angular2 compatibility [#3](https://github.com/johannesjo/angular2-promise-buttons/issues/3) ([52a98e9](https://github.com/johannesjo/angular2-promise-buttons/commit/52a98e9)) 156 | 157 | 158 | 159 | 160 | ## [1.0.6](https://github.com/johannesjo/angular2-promise-buttons/compare/v1.0.5...v1.0.6) (2017-05-10) 161 | 162 | 163 | ### Features 164 | 165 | * simplify code ([009bca9](https://github.com/johannesjo/angular2-promise-buttons/commit/009bca9)) 166 | 167 | 168 | 169 | 170 | ## [1.0.5](https://github.com/johannesjo/angular2-promise-buttons/compare/v1.0.4...v1.0.5) (2017-05-10) 171 | 172 | 173 | ### Features 174 | 175 | * add proper aot support ([7e0186a](https://github.com/johannesjo/angular2-promise-buttons/commit/7e0186a)) 176 | 177 | 178 | 179 | 180 | ## [1.0.4](https://github.com/johannesjo/angular2-promise-buttons/compare/v1.0.3...v1.0.4) (2017-05-10) 181 | 182 | 183 | 184 | 185 | ## [1.0.3](https://github.com/johannesjo/angular2-promise-buttons/compare/v1.0.2...v1.0.3) (2017-05-10) 186 | 187 | 188 | 189 | 190 | ## [1.0.2](https://github.com/johannesjo/angular2-promise-buttons/compare/v1.0.1...v1.0.2) (2017-05-09) 191 | 192 | 193 | 194 | 195 | ## [1.0.1](https://github.com/johannesjo/angular2-promise-buttons/compare/v1.0.0...v1.0.1) (2017-05-09) 196 | 197 | 198 | 199 | 200 | # [1.0.0](https://github.com/johannesjo/angular2-promise-buttons/compare/v0.1.5...v1.0.0) (2017-05-09) 201 | 202 | 203 | ### Bug Fixes 204 | 205 | * add jasmine types again to tsconfig ([e59f8bb](https://github.com/johannesjo/angular2-promise-buttons/commit/e59f8bb)) 206 | * aot and prod build not working ([5e3fd11](https://github.com/johannesjo/angular2-promise-buttons/commit/5e3fd11)) 207 | * build tsconfig for building the module ([885b185](https://github.com/johannesjo/angular2-promise-buttons/commit/885b185)) 208 | * forRoot containing conditional logic ([a6b52f6](https://github.com/johannesjo/angular2-promise-buttons/commit/a6b52f6)) 209 | 210 | 211 | ### Features 212 | 213 | * add readme ([52f84a7](https://github.com/johannesjo/angular2-promise-buttons/commit/52f84a7)) 214 | * use Renderer2 for adding and removing classes ([e2c9555](https://github.com/johannesjo/angular2-promise-buttons/commit/e2c9555)) 215 | 216 | 217 | 218 | 219 | ## [0.1.5](https://github.com/johannesjo/angular2-promise-buttons/compare/v0.1.4...v0.1.5) (2017-05-05) 220 | 221 | 222 | 223 | 224 | ## [0.1.4](https://github.com/johannesjo/angular2-promise-buttons/compare/v0.1.3...v0.1.4) (2017-05-05) 225 | 226 | 227 | 228 | 229 | ## [0.1.3](https://github.com/johannesjo/angular2-promise-buttons/compare/v0.1.2...v0.1.3) (2017-05-05) 230 | 231 | 232 | 233 | 234 | ## [0.1.2](https://github.com/johannesjo/angular2-promise-buttons/compare/v0.1.1...v0.1.2) (2017-05-05) 235 | 236 | 237 | 238 | 239 | ## 0.1.1 (2017-05-05) 240 | 241 | 242 | ### Features 243 | 244 | * make building the module work ([8761ef9](https://github.com/johannesjo/angular2-promise-buttons/commit/8761ef9)) 245 | 246 | 247 | 248 | 249 | ## 0.1.1 (2017-05-05) 250 | 251 | 252 | ### Features 253 | 254 | * make building the module work ([bc36932](https://github.com/johannesjo/angular2-promise-buttons/commit/bc36932)) 255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to this repository 2 | Create a fork of this repository and check it out. 3 | 4 | To make everything work you need to symlink the src directory to the app folder once. You can do this manually or by running `npm run link-mod`. 5 | 6 | Once this is done you can run `npm start` to start the development server. 7 | 8 | If you implement a new feature it is always a good idea to also add an example of it to the demo application. 9 | 10 | ## Commit guidelines 11 | In general this repo tries to adhere to the [angular commit guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit). 12 | 13 | ## Dev tasks 14 | There are several scripts defined in the package.json 15 | 16 | `npm start`: Starts the development server. 17 | 18 | `npm run` **test-watch**: Runs the unit tests via karma. 19 | 20 | `npm run` **test**: Runs the unit tests once. 21 | 22 | `npm run` **test-coverage**: Runs the unit tests with a coverage report. 23 | 24 | `npm run` **build**: Creates a compiled version of your library inside the dist folder. 25 | 26 | `npm run` **demo.deploy**: 27 | Builds the demo app to demo/dist, copies the readme to it and publishes everything to github pages. 28 | 29 | `npm run` **release.changelog**: 30 | Creates a changelog based on the [angular commit conventions](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md). 31 | 32 | `npm run` **link-mod**: Creates a symlink to the module inside the demo/src folder. This is required for compiling the app with aot. 33 | 34 | `npm run` **lint**: Lints all demo and library files 35 | 36 | `npm run` **e2e**: Runs the end2end tests. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Johannes Millan 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 |

2 | 3 |

4 | 5 | npm version 7 | 8 | Build Status 10 | 11 | Coverage Status 13 | 14 | MIT license 16 | 17 |

18 | 19 | 20 | *angular2-promise-buttons* is a simple module that let's you add a loading indicator to a button of your choice. Check out the [demo](http://johannesjo.github.io/angular2-promise-buttons/#demo)! 21 | 22 | [Bug-reports or feature request](https://github.com/johannesjo/angular2-promise-buttons/issues) as well as any other kind of **feedback is highly welcome!** 23 | 24 | ## Getting started 25 | Install it via npm: 26 | ``` 27 | npm install angular2-promise-buttons -S 28 | ``` 29 | 30 | And add it as a dependency to your main module 31 | ```typescript 32 | import {Angular2PromiseButtonModule} from 'angular2-promise-buttons'; 33 | 34 | @NgModule({ 35 | imports: [ 36 | Angular2PromiseButtonModule.forRoot(), 37 | ], 38 | }) 39 | export class MainAppModule { 40 | } 41 | ``` 42 | Using the buttons is easy. Just pass a promise to the directive: 43 | ```html 44 | 46 | ``` 47 | ```typescript 48 | export class SomeComponent { 49 | // some example async action, but this works with any promise 50 | someAction(){ 51 | this.promiseSetBySomeAction = new Promise((resolve, reject) => { 52 | setTimeout(resolve, 2000); 53 | }); 54 | } 55 | } 56 | 57 | ``` 58 | 59 | ## Styling the button 60 | To give you maximum flexibility there are no base styles coming with the directive, but it is easy to fix that! There are lots of free css-spinners out there. Just find one of your liking and add the css to your global stylesheet. 61 | 62 | **Ressources:** 63 | * http://cssload.net/ 64 | * http://projects.lukehaas.me/css-loaders/ 65 | * http://tobiasahlin.com/spinkit/ 66 | 67 | There are selectors you can use to style. There is the `.is-loading` class on the button, which is set, when the promise is pending and there is the `` element inside the button. 68 | 69 | 70 | ## Configuration 71 | Configuration is done via the forRoot method of the promise button module: 72 | ```typescript 73 | import {Angular2PromiseButtonModule} from 'angular2-promise-buttons'; 74 | 75 | @NgModule({ 76 | imports: [ 77 | Angular2PromiseButtonModule 78 | .forRoot({ 79 | // your custom config goes here 80 | spinnerTpl: '', 81 | // disable buttons when promise is pending 82 | disableBtn: true, 83 | // the class used to indicate a pending promise 84 | btnLoadingClass: 'is-loading', 85 | // only disable and show is-loading class for clicked button, 86 | // even when they share the same promise 87 | handleCurrentBtnOnly: false, 88 | }), 89 | ], 90 | }) 91 | export class MainAppModule { 92 | } 93 | ``` 94 | 95 | ## Using observables 96 | When you're using the module with observables make sure to pass a subscription to the directive rather than an observable directly. 97 | ```typescript 98 | const FAKE_FACTORY = { 99 | initObservable: (): Observable => { 100 | return new Observable(observer => { 101 | setTimeout(() => { 102 | observer.complete(); 103 | }, 4000); 104 | }); 105 | } 106 | }; 107 | 108 | // DO: 109 | const observable = FAKE_FACTORY.initObservable(); 110 | this.passedToDirective = observable.subscribe( 111 | // ... 112 | ); 113 | 114 | // DON'T DO: 115 | const observable = FAKE_FACTORY.initObservable(); 116 | this.passedToDirective = observable; 117 | 118 | ``` 119 | 120 | ## Using booleans 121 | Is now also possible. 122 | ```html 123 | 125 | ``` 126 | ## Contributing 127 | Contribution guidelines: [CONTRIBUTING.md](https://github.com/johannesjo/angular2-promise-buttons/blob/master/CONTRIBUTING.md) 128 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular2-promise-buttons-demo": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "projects/angular2-promise-buttons-demo", 14 | "sourceRoot": "projects/angular2-promise-buttons-demo/src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "../../dist/demo", 21 | "index": "projects/angular2-promise-buttons-demo/src/index.html", 22 | "main": "projects/angular2-promise-buttons-demo/src/main.ts", 23 | "polyfills": "projects/angular2-promise-buttons-demo/src/polyfills.ts", 24 | "tsConfig": "projects/angular2-promise-buttons-demo/tsconfig.app.json", 25 | "assets": [ 26 | "projects/angular2-promise-buttons-demo/src/favicon.ico", 27 | "projects/angular2-promise-buttons-demo/src/assets" 28 | ], 29 | "styles": [ 30 | "projects/angular2-promise-buttons-demo/src/styles.scss" 31 | ], 32 | "scripts": [], 33 | "vendorChunk": true, 34 | "extractLicenses": false, 35 | "buildOptimizer": false, 36 | "sourceMap": true, 37 | "optimization": false, 38 | "namedChunks": true 39 | }, 40 | "configurations": { 41 | "production": { 42 | "fileReplacements": [ 43 | { 44 | "replace": "projects/angular2-promise-buttons-demo/src/environments/environment.ts", 45 | "with": "projects/angular2-promise-buttons-demo/src/environments/environment.prod.ts" 46 | } 47 | ], 48 | "optimization": true, 49 | "outputHashing": "all", 50 | "sourceMap": false, 51 | "namedChunks": false, 52 | "extractLicenses": true, 53 | "vendorChunk": false, 54 | "buildOptimizer": true, 55 | "budgets": [ 56 | { 57 | "type": "initial", 58 | "maximumWarning": "2mb", 59 | "maximumError": "5mb" 60 | }, 61 | { 62 | "type": "anyComponentStyle", 63 | "maximumWarning": "6kb" 64 | } 65 | ] 66 | } 67 | }, 68 | "defaultConfiguration": "" 69 | }, 70 | "serve": { 71 | "builder": "@angular-devkit/build-angular:dev-server", 72 | "options": { 73 | "browserTarget": "angular2-promise-buttons-demo:build" 74 | }, 75 | "configurations": { 76 | "production": { 77 | "browserTarget": "angular2-promise-buttons-demo:build:production" 78 | } 79 | } 80 | }, 81 | "extract-i18n": { 82 | "builder": "@angular-devkit/build-angular:extract-i18n", 83 | "options": { 84 | "browserTarget": "angular2-promise-buttons-demo:build" 85 | } 86 | }, 87 | "lint": { 88 | "builder": "@angular-devkit/build-angular:tslint", 89 | "options": { 90 | "tsConfig": [ 91 | "projects/angular2-promise-buttons-demo/tsconfig.app.json", 92 | "projects/angular2-promise-buttons-demo/e2e/tsconfig.json" 93 | ], 94 | "exclude": [ 95 | "**/node_modules/**" 96 | ] 97 | } 98 | }, 99 | "e2e": { 100 | "builder": "@angular-devkit/build-angular:protractor", 101 | "options": { 102 | "protractorConfig": "projects/angular2-promise-buttons-demo/e2e/protractor.conf.js", 103 | "devServerTarget": "angular2-promise-buttons-demo:serve" 104 | }, 105 | "configurations": { 106 | "production": { 107 | "devServerTarget": "angular2-promise-buttons-demo:serve:production" 108 | } 109 | } 110 | } 111 | } 112 | }, 113 | "angular2-promise-buttons": { 114 | "projectType": "library", 115 | "root": "projects/angular2-promise-buttons", 116 | "sourceRoot": "projects/angular2-promise-buttons/src", 117 | "prefix": "lib", 118 | "architect": { 119 | "build": { 120 | "builder": "@angular-devkit/build-angular:ng-packagr", 121 | "options": { 122 | "tsConfig": "projects/angular2-promise-buttons/tsconfig.lib.json", 123 | "project": "projects/angular2-promise-buttons/ng-package.json" 124 | }, 125 | "configurations": { 126 | "production": { 127 | "tsConfig": "projects/angular2-promise-buttons/tsconfig.lib.prod.json" 128 | } 129 | } 130 | }, 131 | "test": { 132 | "builder": "@angular-devkit/build-angular:karma", 133 | "options": { 134 | "main": "projects/angular2-promise-buttons/src/test.ts", 135 | "tsConfig": "projects/angular2-promise-buttons/tsconfig.spec.json", 136 | "karmaConfig": "projects/angular2-promise-buttons/karma.conf.js" 137 | } 138 | }, 139 | "lint": { 140 | "builder": "@angular-devkit/build-angular:tslint", 141 | "options": { 142 | "tsConfig": [ 143 | "projects/angular2-promise-buttons/tsconfig.lib.json", 144 | "projects/angular2-promise-buttons/tsconfig.spec.json" 145 | ], 146 | "exclude": [ 147 | "**/node_modules/**" 148 | ] 149 | } 150 | } 151 | } 152 | } 153 | }, 154 | "defaultProject": "angular2-promise-buttons-demo" 155 | } 156 | -------------------------------------------------------------------------------- /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 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Welcome to angular-material-css-vars!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es2018", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johannesjo/angular2-promise-buttons/eeea9cf2c42c2a17d6bb35f4bd4bb1028046a261/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-promise-buttons", 3 | "version": "6.0.1", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+ssh://git@github.com/johannesjo/angular2-promise-buttons.git" 8 | }, 9 | "scripts": { 10 | "ng": "ng", 11 | "start": "ng serve", 12 | "build": "ng build", 13 | "test": "ng test --browsers ChromeHeadless --watch=false", 14 | "lint": "ng lint", 15 | "e2e": "ng e2e", 16 | "demo": "run-s demo.build demo.copy-readme demo.gh-pages", 17 | "demo.build": "ng build --aot --configuration production --base-href='./'", 18 | "demo.copy-readme": "node scripts/copy-readme-to-demo.js", 19 | "demo.gh-pages": "gh-pages -d dist/demo", 20 | "lib": "run-s lib.build copy", 21 | "lib.build": "ng build --configuration production angular2-promise-buttons", 22 | "copy": "run-s copy.licence copy.readme", 23 | "copy.licence": "copyfiles ./LICENSE ./dist/angular2-promise-buttons", 24 | "copy.readme": "copyfiles ./README.md ./dist/angular2-promise-buttons", 25 | "pub": "run-s lib && cd ./dist/angular2-promise-buttons/ && npm publish && cd .. && cd ..", 26 | "patch": "npm version patch && cd ./projects/angular2-promise-buttons && npm version patch && cd .. && cd .. && git add . && git commit -am\"chore: update lib version\"", 27 | "patch-release_": "run-s lib demo patch pub", 28 | "patch-release": "npm run patch-release_", 29 | "major": "npm version major && cd ./projects/angular2-promise-buttons && npm version major && cd .. && cd .. && git add . && git commit -am\"chore: update lib version\"", 30 | "major-release_": "run-s lib demo major pub", 31 | "major-release": "npm run major-release_", 32 | "test-coverage": "ng test --browsers ChromeHeadless --code-coverage --watch=false", 33 | "coveralls": "YOURPACKAGE_COVERAGE=1 cat ./coverage/lcov.info | ./node_modules/.bin/coveralls" 34 | }, 35 | "peerDependencies": {}, 36 | "devDependencies": { 37 | "@angular-devkit/build-angular": "^12.2.18", 38 | "@angular/cli": "^12.2.18", 39 | "@angular/common": "^12.2.17", 40 | "@angular/compiler": "^12.2.17", 41 | "@angular/compiler-cli": "^12.2.17", 42 | "@angular/core": "^12.2.17", 43 | "@angular/forms": "^12.2.17", 44 | "@angular/platform-browser": "^12.2.17", 45 | "@angular/platform-browser-dynamic": "^12.2.17", 46 | "@angular/router": "^12.2.17", 47 | "@linnenschmidt/build-ng-packagr": "^9.0.0", 48 | "@types/bluebird": "^3.5.29", 49 | "@types/core-js": "^2.5.2", 50 | "@types/jasmine": "~3.6.0", 51 | "@types/jquery": "^3.3.32", 52 | "@types/node": "^13.7.0", 53 | "angular2-template-loader": "^0.6.2", 54 | "bluebird": "^3.7.2", 55 | "bootstrap": "^4.4.1", 56 | "bootstrap-material-design": "^4.1.2", 57 | "codelyzer": "^6.0.0", 58 | "conventional-changelog-cli": "^2.0.31", 59 | "conventional-github-releaser": "^3.1.3", 60 | "copyfiles": "^2.2.0", 61 | "core-js": "^3.6.4", 62 | "coveralls": "^3.0.9", 63 | "gh-pages": "^2.2.0", 64 | "intl": "^1.2.5", 65 | "jasmine-core": "~3.6.0", 66 | "jasmine-spec-reporter": "~5.0.0", 67 | "jquery": "^3.5.0", 68 | "karma": "~6.3.16", 69 | "karma-chrome-launcher": "~3.1.0", 70 | "karma-cli": "~2.0.0", 71 | "karma-coverage-istanbul-reporter": "~3.0.2", 72 | "karma-jasmine": "~4.0.0", 73 | "karma-jasmine-html-reporter": "^1.5.0", 74 | "karma-phantomjs-launcher": "^1.0.4", 75 | "marked": "^2.0.0", 76 | "ng-packagr": "^12.2.7", 77 | "npm-run-all": "^4.1.5", 78 | "protractor": "~7.0.0", 79 | "reflect-metadata": "^0.1.13", 80 | "rxjs": "^6.5.4", 81 | "ts-node": "~8.6.2", 82 | "tslib": "^2.0.0", 83 | "tslint": "~6.1.0", 84 | "typescript": "~4.3.5", 85 | "wallaby-webpack": "3.9.15", 86 | "web-animations-js": "^2.3.2", 87 | "zone.js": "~0.11.4" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | beforeLaunch: function() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | }, 27 | onPrepare() { 28 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../../out-tsc/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/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'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: false, 28 | browsers: ['ChromeHeadless'], 29 | singleRun: true, 30 | customLaunchers: { 31 | 'ChromeHeadless': { 32 | base: 'Chrome', 33 | flags: [ 34 | // We must disable the Chrome sandbox when running Chrome inside Docker 35 | // (Chrome's sandbox needs more permissions than Docker allows by default) 36 | '--headless', 37 | '--no-sandbox', 38 | '--disable-gpu', 39 | '--no-default-browser-check', 40 | '--no-first-run', 41 | '--disable-default-apps', 42 | '--disable-popup-blocking', 43 | '--disable-translate', 44 | '--disable-background-timer-throttling', 45 | '--disable-renderer-backgrounding', 46 | '--disable-device-discovery-notifications', 47 | // Without a remote debugging port, Google Chrome exits immediately. 48 | '--remote-debugging-port=9222', 49 | '--disable-web-security', 50 | ], 51 | debug: true 52 | } 53 | }, 54 | browserNoActivityTimeout: 120000, 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | h2 { 2 | margin-top: 16px; 3 | } 4 | h3 { 5 | margin-top: 16px; 6 | } 7 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 5 | 9 |
10 |
11 | 15 | 18 | 19 |

Same promise buttons

20 | 24 | 28 | 29 |

Chained promise buttons

30 | 34 | 35 | 36 |

Inside a form

37 |
39 | 43 |
44 | 45 |
46 |

Observable

47 | 48 | 52 | 56 |
57 |
58 | 62 | 65 | 66 |

Same observable buttons

67 | 71 | 75 | 76 |

Chained observable button

77 | 81 |
82 | 83 |
84 |

Simple Boolean

85 | 88 | 89 | 92 |
93 | 94 | 95 |
96 |

Dynamic [disabled]

97 | 98 | 101 | 102 | 105 | 106 | 109 |
110 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, TestBed} from '@angular/core/testing'; 2 | import {AppComponent} from './app.component'; 3 | import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; 4 | 5 | // @Directive({ 6 | // selector: '[promiseBtn]' 7 | // }) 8 | // class MockPromiseBtnDirective { 9 | // @Input('promiseBtn') promise: Promise; 10 | // } 11 | 12 | describe('AppComponent', () => { 13 | beforeEach(async(() => { 14 | TestBed.configureTestingModule({ 15 | declarations: [ 16 | AppComponent, 17 | // MockPromiseBtnDirective 18 | ], 19 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 20 | }).compileComponents(); 21 | })); 22 | 23 | it('should create the app', async(() => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.debugElement.componentInstance; 26 | expect(app).toBeTruthy(); 27 | })); 28 | }); 29 | 30 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {Observable, Subscription} from 'rxjs'; 3 | 4 | const STANDARD_DELAY = 1000; 5 | const FAKE_FACT = { 6 | success() { 7 | return new Promise((fulfill) => { 8 | setTimeout(() => { 9 | fulfill({ 10 | msg: 'SUCCESS' 11 | }); 12 | }, STANDARD_DELAY); 13 | }); 14 | }, 15 | error: () => { 16 | return new Promise((fulfill, reject) => { 17 | setTimeout(() => { 18 | reject({ 19 | msg: 'ERROR' 20 | }); 21 | }, STANDARD_DELAY); 22 | }); 23 | }, 24 | endless: () => { 25 | return new Promise((fulfill) => { 26 | setTimeout(fulfill, 99999999); 27 | }); 28 | }, 29 | endlessObservable: (): Observable => { 30 | return new Observable(() => { 31 | }); 32 | }, 33 | initSuccessObservable: (): Observable => { 34 | return new Observable(observer => { 35 | setTimeout(() => { 36 | observer.complete(); 37 | }, STANDARD_DELAY); 38 | }); 39 | }, 40 | initErrorObservable: (): Observable => { 41 | return new Observable(observer => { 42 | setTimeout(() => { 43 | observer.error('ERROR'); 44 | }, STANDARD_DELAY); 45 | }); 46 | }, 47 | initChainedObservable: (): Observable => { 48 | return new Observable(observer => { 49 | setTimeout(() => { 50 | observer.next(1); 51 | }, 1000); 52 | 53 | setTimeout(() => { 54 | observer.next(2); 55 | }, 2000); 56 | 57 | setTimeout(() => { 58 | observer.next(3); 59 | }, 3000); 60 | 61 | setTimeout(() => { 62 | observer.complete(); 63 | }, 4000); 64 | }); 65 | }, 66 | }; 67 | 68 | @Component({ 69 | selector: 'app-root', 70 | templateUrl: './app.component.html', 71 | styleUrls: ['./app.component.css'] 72 | }) 73 | export class AppComponent { 74 | successPromise: Promise; 75 | errorPromise: Promise; 76 | endlessInitialPromise: Promise; 77 | endlessPromise: Promise; 78 | submitPromise: Promise; 79 | chainedPromises: any; 80 | promiseIndex: number; 81 | myBool = true; 82 | 83 | successObservable: Subscription; 84 | errorObservable: Subscription; 85 | endlessInitialObservable: Subscription; 86 | endlessObservable: Subscription; 87 | chainedObservableValue: any; 88 | chainedObservable: Subscription; 89 | customDisabled = true; 90 | myBoolWithCustomDisabled = false; 91 | isOutsideDisabled = true; 92 | 93 | constructor() { 94 | this.endlessInitial(); 95 | this.initEndlessInitialObservable(); 96 | } 97 | 98 | success($event: any): Promise { 99 | console.log($event); 100 | this.successPromise = FAKE_FACT.success(); 101 | return this.successPromise; 102 | } 103 | 104 | error() { 105 | this.errorPromise = FAKE_FACT.error() 106 | .catch(() => { 107 | console.log('YEAH ERROR'); 108 | }); 109 | } 110 | 111 | endless() { 112 | this.endlessPromise = FAKE_FACT.endless(); 113 | } 114 | 115 | endlessInitial() { 116 | this.endlessInitialPromise = FAKE_FACT.endless(); 117 | } 118 | 119 | initSuccessObservable() { 120 | const observable = FAKE_FACT.initSuccessObservable(); 121 | this.successObservable = observable.subscribe( 122 | () => { 123 | }, 124 | () => { 125 | }, 126 | () => { 127 | } 128 | ); 129 | } 130 | 131 | initErrorObservable() { 132 | const observable = FAKE_FACT.initErrorObservable(); 133 | this.errorObservable = observable.subscribe( 134 | () => { 135 | }, 136 | (msg) => { 137 | console.log(msg); 138 | }, 139 | () => { 140 | }, 141 | ); 142 | } 143 | 144 | initChainedObservable() { 145 | const observable = FAKE_FACT.initChainedObservable(); 146 | this.chainedObservableValue = 'INITIALIZED'; 147 | this.chainedObservable = observable.subscribe( 148 | (value: number) => { 149 | this.chainedObservableValue = value; 150 | }, 151 | () => { 152 | }, 153 | () => { 154 | this.chainedObservableValue = 'COMPLETED'; 155 | } 156 | ); 157 | } 158 | 159 | initEndlessObservable() { 160 | const observable = FAKE_FACT.endlessObservable(); 161 | this.endlessObservable = observable.subscribe( 162 | () => { 163 | }, 164 | () => { 165 | }, 166 | () => { 167 | }, 168 | ); 169 | } 170 | 171 | initEndlessInitialObservable() { 172 | const observable = FAKE_FACT.endlessObservable(); 173 | this.endlessInitialObservable = observable.subscribe( 174 | () => { 175 | }, 176 | () => { 177 | }, 178 | () => { 179 | }, 180 | ); 181 | } 182 | 183 | submit() { 184 | this.submitPromise = FAKE_FACT.success(); 185 | } 186 | 187 | chain() { 188 | this.promiseIndex = 0; 189 | this.chainedPromises = this.countChain() 190 | .then(this.countChain.bind(this)) 191 | .then(this.countChain.bind(this)) 192 | .then(this.countChain.bind(this)) 193 | .then(this.countChain.bind(this)); 194 | 195 | return this.chainedPromises; 196 | } 197 | 198 | countChain() { 199 | return FAKE_FACT.success() 200 | .then(() => { 201 | this.promiseIndex++; 202 | }); 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {Angular2PromiseButtonModule} from '../../../angular2-promise-buttons/src'; 2 | import {BrowserModule} from '@angular/platform-browser'; 3 | import {FormsModule} from '@angular/forms'; 4 | import {NgModule} from '@angular/core'; 5 | 6 | import {AppComponent} from './app.component'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | AppComponent, 11 | ], 12 | imports: [ 13 | BrowserModule, 14 | FormsModule, 15 | Angular2PromiseButtonModule 16 | .forRoot({ 17 | // handleCurrentBtnOnly: true, 18 | }), 19 | ], 20 | providers: [], 21 | bootstrap: [AppComponent] 22 | }) 23 | export class AppModule { 24 | } 25 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johannesjo/angular2-promise-buttons/eeea9cf2c42c2a17d6bb35f4bd4bb1028046a261/projects/angular2-promise-buttons-demo/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johannesjo/angular2-promise-buttons/eeea9cf2c42c2a17d6bb35f4bd4bb1028046a261/projects/angular2-promise-buttons-demo/src/favicon.ico -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular2PromiseButtons 6 | 7 | 9 | 12 | 14 | 15 | 16 | 17 |
18 | 20 | Fork me on GitHub 23 | 24 | 25 |
26 |

angular2-promise-buttons

27 |

Chilled Buttons for Angular

28 | 29 | 30 | 31 | Star 37 | 38 | 39 | Fork 45 | 46 | 47 | Issue 53 |
54 |
55 | 56 |
57 |
58 | ___README_MD_NEEDLE___ 59 | 60 | 61 |

Demo

63 | Loading... 64 |
65 |
66 | 67 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js'; 2 | import 'reflect-metadata'; 3 | import { enableProdMode } from '@angular/core'; 4 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 5 | 6 | import { AppModule } from './app/app.module'; 7 | import { environment } from './environments/environment'; 8 | 9 | if (environment.production) { 10 | enableProdMode(); 11 | } 12 | 13 | platformBrowserDynamic().bootstrapModule(AppModule); 14 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/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'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import '../../../node_modules/bootstrap-material-design/dist/css/bootstrap-material-design.css'; 2 | //@import 'node_modules/bootstrap-material-design/scss/bootstrap-material-design'; 3 | 4 | /* You can add global styles to this file, and also import other style files */ 5 | @-webkit-keyframes three-quarters { 6 | 0% { 7 | -webkit-transform: rotate(0deg); 8 | -moz-transform: rotate(0deg); 9 | -ms-transform: rotate(0deg); 10 | -o-transform: rotate(0deg); 11 | transform: rotate(0deg); 12 | } 13 | 14 | 100% { 15 | -webkit-transform: rotate(360deg); 16 | -moz-transform: rotate(360deg); 17 | -ms-transform: rotate(360deg); 18 | -o-transform: rotate(360deg); 19 | transform: rotate(360deg); 20 | } 21 | } 22 | 23 | @-moz-keyframes three-quarters { 24 | 0% { 25 | -webkit-transform: rotate(0deg); 26 | -moz-transform: rotate(0deg); 27 | -ms-transform: rotate(0deg); 28 | -o-transform: rotate(0deg); 29 | transform: rotate(0deg); 30 | } 31 | 32 | 100% { 33 | -webkit-transform: rotate(360deg); 34 | -moz-transform: rotate(360deg); 35 | -ms-transform: rotate(360deg); 36 | -o-transform: rotate(360deg); 37 | transform: rotate(360deg); 38 | } 39 | } 40 | 41 | @-o-keyframes three-quarters { 42 | 0% { 43 | -webkit-transform: rotate(0deg); 44 | -moz-transform: rotate(0deg); 45 | -ms-transform: rotate(0deg); 46 | -o-transform: rotate(0deg); 47 | transform: rotate(0deg); 48 | } 49 | 50 | 100% { 51 | -webkit-transform: rotate(360deg); 52 | -moz-transform: rotate(360deg); 53 | -ms-transform: rotate(360deg); 54 | -o-transform: rotate(360deg); 55 | transform: rotate(360deg); 56 | } 57 | } 58 | 59 | @keyframes three-quarters { 60 | 0% { 61 | -webkit-transform: rotate(0deg); 62 | -moz-transform: rotate(0deg); 63 | -ms-transform: rotate(0deg); 64 | -o-transform: rotate(0deg); 65 | transform: rotate(0deg); 66 | } 67 | 68 | 100% { 69 | -webkit-transform: rotate(360deg); 70 | -moz-transform: rotate(360deg); 71 | -ms-transform: rotate(360deg); 72 | -o-transform: rotate(360deg); 73 | transform: rotate(360deg); 74 | } 75 | } 76 | 77 | /* Styles for old versions of IE */ 78 | button .btn-spinner { 79 | font-family: sans-serif; 80 | font-weight: 100; 81 | -webkit-animation: three-quarters 1250ms infinite linear; 82 | -moz-animation: three-quarters 1250ms infinite linear; 83 | -ms-animation: three-quarters 1250ms infinite linear; 84 | -o-animation: three-quarters 1250ms infinite linear; 85 | animation: three-quarters 1250ms infinite linear; 86 | border: 3px solid #8c024c; 87 | border-right-color: transparent; 88 | border-radius: 100%; 89 | box-sizing: border-box; 90 | display: inline-block; 91 | position: relative; 92 | vertical-align: middle; 93 | overflow: hidden; 94 | text-indent: -9999px; 95 | width: 18px; 96 | height: 18px; 97 | } 98 | 99 | button .btn-spinner:not(:required) { 100 | margin-left: -22px; 101 | opacity: 0; 102 | transition: 0.4s margin ease-out, 103 | 0.2s opacity ease-out; 104 | } 105 | 106 | button.is-loading .btn-spinner { 107 | transition: 0.2s margin ease-in, 108 | 0.4s opacity ease-in; 109 | margin-left: 5px; 110 | opacity: 1; 111 | } 112 | 113 | .btn { 114 | text-align: left; 115 | } 116 | 117 | body { 118 | padding-bottom: 50px; 119 | } 120 | 121 | .main-header { 122 | background: #3f51b5; 123 | color: #eeeeee; 124 | height: 133px; 125 | } 126 | 127 | .main-header h1 { 128 | margin-top: 10px; 129 | } 130 | 131 | .fork-me-badge { 132 | position: absolute; 133 | right: 0; 134 | top: 0; 135 | } 136 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/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/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons-demo/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "align": { 5 | "options": [ 6 | "parameters", 7 | "statements" 8 | ] 9 | }, 10 | "array-type": false, 11 | "arrow-parens": false, 12 | "arrow-return-shorthand": true, 13 | "deprecation": { 14 | "severity": "warning" 15 | }, 16 | "component-class-suffix": true, 17 | "contextual-lifecycle": true, 18 | "curly": true, 19 | "directive-class-suffix": true, 20 | "directive-selector": [ 21 | false, 22 | "attribute", 23 | "app", 24 | "camelCase" 25 | ], 26 | "component-selector": [ 27 | false, 28 | "element", 29 | "app", 30 | "kebab-case" 31 | ], 32 | "eofline": true, 33 | "import-blacklist": [ 34 | true, 35 | "rxjs/Rx" 36 | ], 37 | "import-spacing": true, 38 | "indent": { 39 | "options": [ 40 | "spaces" 41 | ] 42 | }, 43 | "interface-name": false, 44 | "max-classes-per-file": false, 45 | "max-line-length": [ 46 | true, 47 | 150 48 | ], 49 | "member-access": false, 50 | "member-ordering": [ 51 | true, 52 | { 53 | "order": [ 54 | "static-field", 55 | "instance-field", 56 | "static-method", 57 | "instance-method" 58 | ] 59 | } 60 | ], 61 | "no-consecutive-blank-lines": false, 62 | "no-console": [ 63 | true, 64 | "debug", 65 | "info", 66 | "time", 67 | "timeEnd", 68 | "trace" 69 | ], 70 | "no-empty": false, 71 | "no-inferrable-types": [ 72 | true, 73 | "ignore-params" 74 | ], 75 | "no-non-null-assertion": true, 76 | "no-redundant-jsdoc": true, 77 | "no-switch-case-fall-through": true, 78 | "no-var-requires": false, 79 | "object-literal-key-quotes": [ 80 | true, 81 | "as-needed" 82 | ], 83 | "object-literal-sort-keys": false, 84 | "ordered-imports": false, 85 | "quotemark": [ 86 | true, 87 | "single" 88 | ], 89 | "trailing-comma": false, 90 | "no-conflicting-lifecycle": true, 91 | "no-host-metadata-property": true, 92 | "no-input-rename": false, 93 | "no-inputs-metadata-property": true, 94 | "no-output-native": true, 95 | "no-output-on-prefix": true, 96 | "no-output-rename": true, 97 | "semicolon": { 98 | "options": [ 99 | "always" 100 | ] 101 | }, 102 | "space-before-function-paren": { 103 | "options": { 104 | "anonymous": "never", 105 | "asyncArrow": "always", 106 | "constructor": "never", 107 | "method": "never", 108 | "named": "never" 109 | } 110 | }, 111 | "no-outputs-metadata-property": true, 112 | "template-banana-in-box": true, 113 | "template-no-negated-async": false, 114 | "typedef-whitespace": { 115 | "options": [ 116 | { 117 | "call-signature": "nospace", 118 | "index-signature": "nospace", 119 | "parameter": "nospace", 120 | "property-declaration": "nospace", 121 | "variable-declaration": "nospace" 122 | }, 123 | { 124 | "call-signature": "onespace", 125 | "index-signature": "onespace", 126 | "parameter": "onespace", 127 | "property-declaration": "onespace", 128 | "variable-declaration": "onespace" 129 | } 130 | ] 131 | }, 132 | "use-lifecycle-interface": true, 133 | "use-pipe-transform-interface": true, 134 | "variable-name": [ 135 | true, 136 | "allow-pascal-case", 137 | "allow-snake-case", 138 | "ban-keywords", 139 | "check-format", 140 | "allow-leading-underscore" 141 | ], 142 | "whitespace": { 143 | "options": [ 144 | "check-branch", 145 | "check-decl", 146 | "check-operator", 147 | "check-separator", 148 | "check-type", 149 | "check-typecast" 150 | ] 151 | } 152 | }, 153 | "rulesDirectory": [ 154 | "codelyzer" 155 | ] 156 | } 157 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons/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/angular2-promise-buttons'), 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/angular2-promise-buttons/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/angular2-promise-buttons", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | }, 7 | "assets": [ 8 | "./**/*.scss" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-promise-buttons", 3 | "version": "6.0.0", 4 | "description": "Chilled loading buttons for angular", 5 | "author": "johannesjo (http://super-productivity.com)", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+ssh://git@github.com/johannesjo/angular2-promise-buttons.git" 10 | }, 11 | "keywords": [ 12 | "angular", 13 | "javascript", 14 | "typescript", 15 | "button", 16 | "promise", 17 | "spinner" 18 | ], 19 | "dependencies": { 20 | "tslib": "^2.0.0" 21 | }, 22 | "peerDependencies": { 23 | "@angular/common": ">=9.0.4", 24 | "@angular/core": ">=9.0.4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons/src/angular2-promise-buttons.module.ts: -------------------------------------------------------------------------------- 1 | import {ModuleWithProviders, NgModule} from '@angular/core'; 2 | import {PromiseBtnDirective} from './promise-btn.directive'; 3 | import {PromiseBtnConfig} from './promise-btn-config'; 4 | import {userCfg} from './user-cfg'; 5 | 6 | @NgModule({ 7 | declarations: [ 8 | PromiseBtnDirective, 9 | ], 10 | imports: [], 11 | exports: [ 12 | PromiseBtnDirective, 13 | ], 14 | providers: [] 15 | }) 16 | export class Angular2PromiseButtonModule { 17 | // add forRoot to make it configurable 18 | static forRoot(config?: PromiseBtnConfig): ModuleWithProviders { 19 | // NOTE: this is never allowed to contain any conditional logic 20 | return { 21 | ngModule: Angular2PromiseButtonModule, 22 | providers: [{provide: userCfg, useValue: config}] 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons/src/default-promise-btn-config.ts: -------------------------------------------------------------------------------- 1 | import {PromiseBtnConfig} from './promise-btn-config'; 2 | 3 | export const DEFAULT_CFG: PromiseBtnConfig = { 4 | spinnerTpl: '', 5 | disableBtn: true, 6 | btnLoadingClass: 'is-loading', 7 | handleCurrentBtnOnly: false, 8 | minDuration: null, 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons/src/index.ts: -------------------------------------------------------------------------------- 1 | export {PromiseBtnDirective} from './promise-btn.directive'; 2 | export {Angular2PromiseButtonModule} from './angular2-promise-buttons.module'; 3 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons/src/promise-btn-config.ts: -------------------------------------------------------------------------------- 1 | export interface PromiseBtnConfig { 2 | spinnerTpl?: string; 3 | disableBtn?: boolean; 4 | btnLoadingClass?: boolean | string; 5 | handleCurrentBtnOnly?: boolean; 6 | minDuration?: number | null; 7 | } 8 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons/src/promise-btn.directive.spec.ts: -------------------------------------------------------------------------------- 1 | // import 'core-js/fn/object/entries'; 2 | import {Component, DebugElement, ElementRef} from '@angular/core'; 3 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 4 | import {PromiseBtnDirective} from './promise-btn.directive'; 5 | import {userCfg} from './user-cfg'; 6 | import {By} from '@angular/platform-browser'; 7 | import {Observable} from 'rxjs'; 8 | import {delay} from 'rxjs/operators'; 9 | import * as BlueBird from 'bluebird'; 10 | import * as jQuery from 'jquery'; 11 | 12 | class MockElementRef extends ElementRef { 13 | constructor() { 14 | super(null); 15 | this.nativeElement = {}; 16 | } 17 | } 18 | 19 | @Component({ 20 | selector: 'test-component', 21 | template: '' 22 | }) 23 | class TestComponent { 24 | testPromise: any; 25 | setPromise: any; 26 | isDisabled: any; 27 | } 28 | 29 | 30 | let testUserCfg: any; 31 | 32 | describe('PromiseBtnDirective', () => { 33 | beforeEach(async(() => { 34 | testUserCfg = {}; 35 | TestBed.configureTestingModule({ 36 | declarations: [ 37 | TestComponent, 38 | PromiseBtnDirective 39 | ], 40 | providers: [ 41 | // more providers 42 | { 43 | provide: ElementRef, 44 | useClass: MockElementRef 45 | }, 46 | { 47 | provide: userCfg, useValue: testUserCfg 48 | }, 49 | ] 50 | }); 51 | })); 52 | 53 | describe('runtimeCfg', () => { 54 | let fixture: ComponentFixture; 55 | let buttonDebugElement: DebugElement; 56 | let buttonElement: HTMLButtonElement; 57 | let promiseBtnDirective: PromiseBtnDirective; 58 | 59 | beforeEach(() => { 60 | fixture = TestBed.overrideComponent(TestComponent, { 61 | set: { 62 | template: '' 63 | } 64 | }).createComponent(TestComponent); 65 | fixture.detectChanges(); 66 | 67 | buttonDebugElement = fixture.debugElement.query(By.css('button')); 68 | buttonElement = (buttonDebugElement.nativeElement as HTMLButtonElement); 69 | promiseBtnDirective = buttonDebugElement.injector.get(PromiseBtnDirective); 70 | }); 71 | 72 | describe('default cfg', () => { 73 | describe('basic init', () => { 74 | it('should create an instance', () => { 75 | expect(promiseBtnDirective).toBeDefined(); 76 | expect(promiseBtnDirective.cfg).toBeDefined(); 77 | // const directive = new PromiseBtnDirective({}, {}); 78 | // expect(directive).toBeTruthy(); 79 | }); 80 | it('should append the spinner el to the button', () => { 81 | const spinnerEl = buttonElement.querySelector('span'); 82 | expect(spinnerEl && spinnerEl.outerHTML).toBe(''); 83 | }); 84 | describe('should accept all promise-alike values', () => { 85 | const possibleValues = { 86 | 'native Promise': () => new Promise((resolve) => { 87 | resolve(); 88 | }), 89 | 'jQuery Deferred': () => jQuery.Deferred((defer: any) => { 90 | defer.resolve(); 91 | }), 92 | 'jQuery Deferred Promise': () => jQuery.Deferred((defer: any) => { 93 | defer.resolve(); 94 | }).promise(), 95 | 'bluebird Promise': () => new BlueBird((resolve) => { 96 | resolve(); 97 | }), 98 | 'RxJs Observable': () => { 99 | const observable = new Observable((subscriber) => { 100 | subscriber.complete(); 101 | }); 102 | 103 | return observable.pipe(delay(0)).subscribe( 104 | () => { 105 | }, 106 | () => { 107 | }, 108 | () => { 109 | }, 110 | ); 111 | }, 112 | }; 113 | 114 | // Iterate over possible values 115 | for (const [description, getPromise] of (Object as any).entries(possibleValues)) { 116 | describe(`testing ${description}`, () => { 117 | beforeEach(() => { 118 | fixture.componentInstance.testPromise = getPromise(); 119 | // test init before to be sure 120 | spyOn(promiseBtnDirective, 'initLoadingState').and.callThrough(); 121 | fixture.detectChanges(); 122 | }); 123 | 124 | it('should init the loading state', () => { 125 | expect(promiseBtnDirective.initLoadingState).toHaveBeenCalled(); 126 | }); 127 | }); 128 | } 129 | }); 130 | it('should convert RxJs Observable Subscription to Promise', () => { 131 | const observable = new Observable((subscriber) => { 132 | subscriber.complete(); 133 | }); 134 | fixture.componentInstance.testPromise = observable.pipe(delay(0)).subscribe( 135 | () => { 136 | }, 137 | () => { 138 | }, 139 | () => { 140 | }, 141 | ); 142 | fixture.detectChanges(); 143 | expect(promiseBtnDirective.promise instanceof Promise).toBe(true); 144 | }); 145 | it('should throw an error if an observable is passed directly', () => { 146 | const observable = new Observable((subscriber) => { 147 | subscriber.complete(); 148 | }); 149 | fixture.componentInstance.testPromise = observable; 150 | 151 | expect(() => { 152 | fixture.detectChanges(); 153 | }).toThrowError('promiseBtn must be an instance of Subscription, instance of Observable given'); 154 | }); 155 | it('should do nothing with a closed subscription', () => { 156 | spyOn(promiseBtnDirective, 'initLoadingState'); 157 | 158 | const observable = new Observable((subscriber) => { 159 | subscriber.complete(); 160 | }); 161 | // subscription will immediately complete and close 162 | fixture.componentInstance.testPromise = observable.subscribe( 163 | () => { 164 | }, 165 | () => { 166 | }, 167 | () => { 168 | }, 169 | ); 170 | fixture.detectChanges(); 171 | 172 | expect(promiseBtnDirective.promise).toBe(undefined); 173 | expect(promiseBtnDirective.initLoadingState).not.toHaveBeenCalled(); 174 | }); 175 | }); 176 | 177 | describe('when promise is passed after click', () => { 178 | beforeEach(() => { 179 | fixture.componentInstance.setPromise = () => { 180 | fixture.componentInstance.testPromise = new Promise(() => { 181 | }); 182 | }; 183 | fixture.detectChanges(); 184 | 185 | // remove initial promise 186 | fixture.componentInstance.testPromise = null; 187 | fixture.detectChanges(); 188 | 189 | // test init before to be sure 190 | spyOn(promiseBtnDirective, 'initLoadingState').and.callThrough(); 191 | fixture.detectChanges(); 192 | 193 | buttonDebugElement.triggerEventHandler('click', null); 194 | fixture.detectChanges(); 195 | }); 196 | 197 | it('should init the loading state', () => { 198 | expect(promiseBtnDirective.initLoadingState).toHaveBeenCalled(); 199 | }); 200 | it('should add .is-loading class', async(() => { 201 | fixture.whenStable().then(() => { 202 | expect(buttonElement.className).toBe('is-loading'); 203 | }); 204 | })); 205 | it('should disable the button', async(() => { 206 | fixture.whenStable().then(() => { 207 | expect(buttonElement.getAttribute('disabled')).toBe('disabled'); 208 | }); 209 | })); 210 | }); 211 | 212 | describe('once a promise is passed', () => { 213 | beforeEach(() => { 214 | fixture.componentInstance.testPromise = new Promise(() => { 215 | }); 216 | spyOn(promiseBtnDirective, 'initLoadingState').and.callThrough(); 217 | fixture.detectChanges(); 218 | }); 219 | 220 | it('should init the loading state', () => { 221 | expect(promiseBtnDirective.initLoadingState).toHaveBeenCalled(); 222 | }); 223 | it('should add .is-loading class', async(() => { 224 | fixture.whenStable().then(() => { 225 | expect(buttonElement.className).toBe('is-loading'); 226 | }); 227 | })); 228 | it('should disable the button', async(() => { 229 | fixture.whenStable().then(() => { 230 | expect(buttonElement.getAttribute('disabled')).toBe('disabled'); 231 | }); 232 | })); 233 | }); 234 | 235 | describe('once a passed promise is resolved', () => { 236 | let promise; 237 | let resolve: any; 238 | beforeEach(async(() => { 239 | promise = new Promise((res) => { 240 | resolve = res; 241 | }); 242 | fixture.componentInstance.testPromise = promise; 243 | 244 | // test init before to be sure 245 | spyOn(promiseBtnDirective, 'initLoadingState').and.callThrough(); 246 | fixture.detectChanges(); 247 | expect(promiseBtnDirective.initLoadingState).toHaveBeenCalled(); 248 | 249 | fixture.whenStable().then(() => { 250 | spyOn(promiseBtnDirective, 'cancelLoadingStateIfPromiseAndMinDurationDone').and.callThrough(); 251 | resolve(); 252 | }); 253 | fixture.detectChanges(); 254 | })); 255 | 256 | it('should cancel the loading state', () => { 257 | expect(promiseBtnDirective.cancelLoadingStateIfPromiseAndMinDurationDone).toHaveBeenCalled(); 258 | }); 259 | it('should remove the .is-loading class', () => { 260 | expect(buttonElement.className).toBe(''); 261 | }); 262 | it('should enable the button', () => { 263 | expect(buttonElement.hasAttribute('disabled')).toBe(false); 264 | }); 265 | }); 266 | 267 | describe('once a passed promise is rejected', () => { 268 | let promise; 269 | let reject: any; 270 | beforeEach(async(() => { 271 | promise = new Promise((res, rej) => { 272 | reject = rej; 273 | }); 274 | fixture.componentInstance.testPromise = promise; 275 | 276 | // test init before to be sure 277 | spyOn(promiseBtnDirective, 'initLoadingState').and.callThrough(); 278 | fixture.detectChanges(); 279 | expect(promiseBtnDirective.initLoadingState).toHaveBeenCalled(); 280 | 281 | fixture.whenStable().then(() => { 282 | spyOn(promiseBtnDirective, 'cancelLoadingStateIfPromiseAndMinDurationDone').and.callThrough(); 283 | reject(); 284 | }); 285 | fixture.detectChanges(); 286 | })); 287 | 288 | it('should cancel the loading state', () => { 289 | expect(promiseBtnDirective.cancelLoadingStateIfPromiseAndMinDurationDone).toHaveBeenCalled(); 290 | }); 291 | it('should remove the .is-loading class', () => { 292 | expect(buttonElement.className).toBe(''); 293 | }); 294 | it('should enable the button', () => { 295 | expect(buttonElement.hasAttribute('disabled')).toBe(false); 296 | }); 297 | }); 298 | 299 | describe('should do nothing when anything else than a promise is passed', () => { 300 | const possibleValues = { 301 | undefined, 302 | null: null, 303 | boolean: false, 304 | number: 1, 305 | NaN, 306 | array: [], 307 | object: {}, 308 | 'object, "then" is not a function': {then: true}, 309 | 'object, "then" is invalid function': { 310 | then: () => { 311 | } 312 | }, 313 | }; 314 | 315 | // Iterate over possible values 316 | for (const [description, promise] of (Object as any).entries(possibleValues)) { 317 | describe(`testing ${description}`, () => { 318 | beforeEach(() => { 319 | fixture.componentInstance.testPromise = promise; 320 | // test init before to be sure 321 | spyOn(promiseBtnDirective, 'initLoadingState').and.callThrough(); 322 | fixture.detectChanges(); 323 | }); 324 | 325 | it('should cancel the loading state', () => { 326 | expect(promiseBtnDirective.initLoadingState).not.toHaveBeenCalled(); 327 | }); 328 | it('should remove the .is-loading class', () => { 329 | expect(buttonElement.className).toBe(''); 330 | }); 331 | it('should enable the button', () => { 332 | expect(buttonElement.hasAttribute('disabled')).toBe(false); 333 | }); 334 | }); 335 | } 336 | }); 337 | }); 338 | 339 | describe('cfg:minDuration', () => { 340 | describe('once a passed promise is resolved but minDuration has not been exceeded', () => { 341 | let promise; 342 | let resolve: any; 343 | beforeEach((done) => { 344 | promiseBtnDirective.cfg.minDuration = 300; 345 | promise = new Promise((res) => { 346 | resolve = res; 347 | }); 348 | fixture.componentInstance.testPromise = promise; 349 | 350 | // test init before to be sure 351 | spyOn(promiseBtnDirective, 'initLoadingState').and.callThrough(); 352 | fixture.detectChanges(); 353 | expect(promiseBtnDirective.initLoadingState).toHaveBeenCalled(); 354 | 355 | spyOn(promiseBtnDirective, 'cancelLoadingStateIfPromiseAndMinDurationDone').and.callThrough(); 356 | setTimeout(() => { 357 | resolve(); 358 | setTimeout(() => { 359 | done(); 360 | }, 10); 361 | }, 10); 362 | }); 363 | 364 | it('should try to cancel the loading state', () => { 365 | expect(promiseBtnDirective.cancelLoadingStateIfPromiseAndMinDurationDone).toHaveBeenCalled(); 366 | }); 367 | it('should not yet remove the .is-loading class', () => { 368 | expect(buttonElement.className).toBe('is-loading'); 369 | }); 370 | it('should not yet enable the button', () => { 371 | expect(buttonElement.hasAttribute('disabled')).toBe(true); 372 | }); 373 | }); 374 | 375 | describe('once a passed promise is resolved and the minDuration has been exceeded', () => { 376 | let promise; 377 | let resolve: any; 378 | beforeEach((done) => { 379 | promiseBtnDirective.cfg.minDuration = 30; 380 | promise = new Promise((res) => { 381 | resolve = res; 382 | }); 383 | fixture.componentInstance.testPromise = promise; 384 | 385 | // test init before to be sure 386 | spyOn(promiseBtnDirective, 'initLoadingState').and.callThrough(); 387 | fixture.detectChanges(); 388 | expect(promiseBtnDirective.initLoadingState).toHaveBeenCalled(); 389 | 390 | spyOn(promiseBtnDirective, 'cancelLoadingStateIfPromiseAndMinDurationDone').and.callThrough(); 391 | setTimeout(() => { 392 | resolve(); 393 | setTimeout(() => { 394 | done(); 395 | }, ((promiseBtnDirective.cfg.minDuration as number) + 5)); 396 | }, 10); 397 | }); 398 | 399 | it('should try to cancel the loading state', () => { 400 | expect(promiseBtnDirective.cancelLoadingStateIfPromiseAndMinDurationDone).toHaveBeenCalled(); 401 | }); 402 | it('should remove the .is-loading class', () => { 403 | expect(buttonElement.className).toBe(''); 404 | }); 405 | it('should enable the button', () => { 406 | expect(buttonElement.hasAttribute('disabled')).toBe(false); 407 | }); 408 | }); 409 | }); 410 | 411 | describe('cfg:disableBtn:false once a promise is passed', () => { 412 | beforeEach(() => { 413 | promiseBtnDirective.cfg.disableBtn = false; 414 | fixture.componentInstance.testPromise = new Promise(() => { 415 | }); 416 | spyOn(promiseBtnDirective, 'initLoadingState').and.callThrough(); 417 | fixture.detectChanges(); 418 | }); 419 | 420 | it('should init the loading state', () => { 421 | expect(promiseBtnDirective.initLoadingState).toHaveBeenCalled(); 422 | }); 423 | it('should NOT disable the button', async(() => { 424 | expect(buttonElement.hasAttribute('disabled')).toBe(false); 425 | })); 426 | }); 427 | 428 | describe('cfg:btnLoadingClass once a promise is passed', () => { 429 | it('should add a custom loading class', async(() => { 430 | spyOn(promiseBtnDirective, 'addLoadingClass').and.callThrough(); 431 | promiseBtnDirective.cfg.btnLoadingClass = 'TEST'; 432 | 433 | fixture.componentInstance.testPromise = new Promise(() => { 434 | }); 435 | fixture.detectChanges(); 436 | 437 | fixture.whenStable().then(() => { 438 | expect(promiseBtnDirective.addLoadingClass).toHaveBeenCalled(); 439 | expect(buttonElement.className).toBe('TEST'); 440 | }); 441 | })); 442 | it('should not add a loading class if set to false', async(() => { 443 | spyOn(promiseBtnDirective, 'addLoadingClass').and.callThrough(); 444 | 445 | promiseBtnDirective.cfg.btnLoadingClass = false; 446 | fixture.componentInstance.testPromise = new Promise(() => { 447 | }); 448 | fixture.detectChanges(); 449 | 450 | fixture.whenStable().then(() => { 451 | expect(buttonElement.className).toBe(''); 452 | }); 453 | })); 454 | }); 455 | }); 456 | 457 | describe('cfg:handleCurrentBtnOnly', () => { 458 | let fixture: ComponentFixture; 459 | let buttonDebugElement: DebugElement; 460 | let divDebugElement: DebugElement; 461 | let buttonElement: HTMLButtonElement; 462 | let divElement: HTMLDivElement; 463 | let promiseBtnDirective1: PromiseBtnDirective; 464 | let promiseBtnDirective2: PromiseBtnDirective; 465 | 466 | beforeEach(() => { 467 | testUserCfg.handleCurrentBtnOnly = true; 468 | 469 | fixture = TestBed.overrideComponent(TestComponent, { 470 | set: { 471 | template: '
2
' 472 | } 473 | }).createComponent(TestComponent); 474 | fixture.detectChanges(); 475 | buttonDebugElement = fixture.debugElement.query(By.css('button')); 476 | divDebugElement = fixture.debugElement.query(By.css('div')); 477 | buttonElement = (buttonDebugElement.nativeElement as HTMLButtonElement); 478 | divElement = (divDebugElement.nativeElement as HTMLDivElement); 479 | promiseBtnDirective1 = buttonDebugElement.injector.get(PromiseBtnDirective); 480 | promiseBtnDirective2 = divDebugElement.injector.get(PromiseBtnDirective); 481 | fixture.detectChanges(); 482 | 483 | promiseBtnDirective1.cfg.handleCurrentBtnOnly = true; 484 | promiseBtnDirective2.cfg.handleCurrentBtnOnly = true; 485 | 486 | fixture.componentInstance.testPromise = new Promise(() => { 487 | }); 488 | 489 | spyOn(promiseBtnDirective1, 'initLoadingState').and.callThrough(); 490 | spyOn(promiseBtnDirective2, 'initLoadingState').and.callThrough(); 491 | spyOn(promiseBtnDirective1, 'handleCurrentBtnOnly').and.callThrough(); 492 | fixture.detectChanges(); 493 | }); 494 | 495 | it('should cancel the click handler when handleCurrentBtnOnly is false', async(() => { 496 | promiseBtnDirective1.cfg.handleCurrentBtnOnly = false; 497 | buttonElement.click(); 498 | fixture.detectChanges(); 499 | 500 | fixture.whenStable().then(() => { 501 | expect(promiseBtnDirective1.handleCurrentBtnOnly()).toBe(true); 502 | expect(promiseBtnDirective1.initLoadingState).not.toHaveBeenCalled(); 503 | }); 504 | })); 505 | 506 | it('should set loading state for first button when clicked, but not for second', async(() => { 507 | buttonElement.click(); 508 | fixture.detectChanges(); 509 | fixture.whenStable().then(() => { 510 | expect(promiseBtnDirective1.initLoadingState).toHaveBeenCalled(); 511 | expect(promiseBtnDirective2.initLoadingState).not.toHaveBeenCalled(); 512 | }); 513 | })); 514 | 515 | it('should set loading state for second button when clicked, but not for first', async(() => { 516 | divElement.click(); 517 | fixture.detectChanges(); 518 | fixture.whenStable().then(() => { 519 | expect(promiseBtnDirective1.initLoadingState).not.toHaveBeenCalled(); 520 | expect(promiseBtnDirective2.initLoadingState).toHaveBeenCalled(); 521 | }); 522 | })); 523 | 524 | it('should set loading state when promise is set after click', async(() => { 525 | const setPromise = () => { 526 | fixture.componentInstance.testPromise = new Promise(() => { 527 | }); 528 | }; 529 | 530 | // remove initial promise 531 | fixture.componentInstance.testPromise = null; 532 | fixture.detectChanges(); 533 | 534 | // add promise on click 535 | buttonElement.addEventListener('click', setPromise); 536 | fixture.detectChanges(); 537 | 538 | buttonElement.click(); 539 | fixture.detectChanges(); 540 | 541 | fixture.whenStable().then(() => { 542 | expect(promiseBtnDirective1.initLoadingState).toHaveBeenCalled(); 543 | 544 | // cleanup 545 | buttonElement.removeEventListener('click', setPromise); 546 | }); 547 | })); 548 | }); 549 | 550 | describe('cfg before runtime', () => { 551 | let fixture: ComponentFixture; 552 | let buttonDebugElement: DebugElement; 553 | let buttonElement: HTMLButtonElement; 554 | let promiseBtnDirective: PromiseBtnDirective; 555 | 556 | describe('cfg:spinnerTpl', () => { 557 | it('should add a custom template from config', () => { 558 | testUserCfg.spinnerTpl = '
loading
'; 559 | 560 | fixture = TestBed.overrideComponent(TestComponent, { 561 | set: { 562 | template: '' 563 | } 564 | }).createComponent(TestComponent); 565 | fixture.detectChanges(); 566 | buttonDebugElement = fixture.debugElement.query(By.css('button')); 567 | buttonElement = (buttonDebugElement.nativeElement as HTMLButtonElement); 568 | promiseBtnDirective = buttonDebugElement.injector.get(PromiseBtnDirective); 569 | fixture.detectChanges(); 570 | 571 | const spinnerEl = buttonElement.querySelector('div'); 572 | expect(spinnerEl && spinnerEl.outerHTML).toBe('
loading
'); 573 | }); 574 | }); 575 | }); 576 | 577 | 578 | describe('simple boolean', () => { 579 | let fixture: ComponentFixture; 580 | 581 | it('should not remove', () => { 582 | fixture = TestBed.overrideComponent(TestComponent, { 583 | set: { 584 | template: '' 585 | } 586 | }).createComponent(TestComponent); 587 | const buttonDebugElement = fixture.debugElement.query(By.css('button')); 588 | const buttonElement = (buttonDebugElement.nativeElement as HTMLButtonElement); 589 | fixture.componentInstance.isDisabled = true; 590 | 591 | fixture.detectChanges(); 592 | expect(buttonElement.hasAttribute('disabled')).toBe(true); 593 | }); 594 | }); 595 | }); 596 | 597 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons/src/promise-btn.directive.ts: -------------------------------------------------------------------------------- 1 | import {AfterContentInit, Directive, ElementRef, HostListener, Inject, Input, OnDestroy} from '@angular/core'; 2 | import {Observable, Subscription} from 'rxjs'; 3 | import {DEFAULT_CFG} from './default-promise-btn-config'; 4 | import {PromiseBtnConfig} from './promise-btn-config'; 5 | import {userCfg} from './user-cfg'; 6 | 7 | @Directive({ 8 | selector: '[promiseBtn]' 9 | }) 10 | 11 | export class PromiseBtnDirective implements OnDestroy, AfterContentInit { 12 | cfg: PromiseBtnConfig; 13 | // the timeout used for min duration display 14 | minDurationTimeout: number; 15 | // boolean to determine minDurationTimeout state 16 | isMinDurationTimeoutDone: boolean; 17 | // boolean to determine if promise was resolved 18 | isPromiseDone: boolean; 19 | // the promise button button element 20 | btnEl: HTMLElement; 21 | // the promise itself or a function expression 22 | // NOTE: we need the type any here as we might deal with custom promises like bluebird 23 | promise: any; 24 | 25 | // this is added to fix the overriding of the disabled state by the loading indicator button. 26 | // https://github.com/johannesjo/angular2-promise-buttons/issues/34 27 | @Input('disabled') 28 | set isDisabledFromTheOutsideSetter(v: boolean) { 29 | this.isDisabledFromTheOutside = v; 30 | if (v) { 31 | // disabled means always disabled 32 | this.btnEl.setAttribute('disabled', 'disabled'); 33 | } else if (this.isPromiseDone || this.isPromiseDone === undefined) { 34 | this.btnEl.removeAttribute('disabled'); 35 | } 36 | // else the button is loading, so do not change the disabled loading state. 37 | } 38 | 39 | isDisabledFromTheOutside: boolean; 40 | 41 | private _fakePromiseResolve: (value: void) => void; 42 | 43 | constructor(el: ElementRef, 44 | @Inject(userCfg) cfg: PromiseBtnConfig) { 45 | // provide configuration 46 | this.cfg = Object.assign({}, DEFAULT_CFG, cfg); 47 | 48 | // save element 49 | this.btnEl = el.nativeElement; 50 | } 51 | 52 | @Input() 53 | set promiseBtn(passedValue: any) { 54 | const isObservable: boolean = passedValue instanceof Observable; 55 | const isSubscription: boolean = passedValue instanceof Subscription; 56 | const isBoolean: boolean = typeof passedValue === 'boolean'; 57 | const isPromise: boolean = passedValue instanceof Promise || ( 58 | passedValue !== null && 59 | typeof passedValue === 'object' && 60 | typeof passedValue.then === 'function' && 61 | typeof passedValue.catch === 'function' 62 | ); 63 | 64 | if (isObservable) { 65 | throw new TypeError('promiseBtn must be an instance of Subscription, instance of Observable given'); 66 | } else if (isSubscription) { 67 | const sub: Subscription = passedValue; 68 | if (!sub.closed) { 69 | this.promise = new Promise((resolve) => { 70 | sub.add(resolve); 71 | }); 72 | } 73 | } else if (isPromise) { 74 | this.promise = passedValue; 75 | } else if (isBoolean) { 76 | this.promise = this.createPromiseFromBoolean(passedValue); 77 | } 78 | 79 | this.checkAndInitPromiseHandler(this.btnEl); 80 | } 81 | 82 | ngAfterContentInit() { 83 | this.prepareBtnEl(this.btnEl); 84 | // trigger changes once to handle initial promises 85 | this.checkAndInitPromiseHandler(this.btnEl); 86 | } 87 | 88 | ngOnDestroy() { 89 | // cleanup 90 | if (this.minDurationTimeout) { 91 | clearTimeout(this.minDurationTimeout); 92 | } 93 | } 94 | 95 | createPromiseFromBoolean(val: boolean): Promise { 96 | if (val) { 97 | return new Promise((resolve) => { 98 | this._fakePromiseResolve = resolve; 99 | }); 100 | } else { 101 | if (this._fakePromiseResolve) { 102 | this._fakePromiseResolve(); 103 | } 104 | return this.promise; 105 | } 106 | } 107 | 108 | /** 109 | * Initializes all html and event handlers 110 | */ 111 | prepareBtnEl(btnEl: HTMLElement) { 112 | // handle promises passed via promiseBtn attribute 113 | this.appendSpinnerTpl(btnEl); 114 | } 115 | 116 | /** 117 | * Checks if all required parameters are there and inits the promise handler 118 | */ 119 | checkAndInitPromiseHandler(btnEl: HTMLElement) { 120 | // check if element and promise is set 121 | if (btnEl && this.promise) { 122 | this.initPromiseHandler(btnEl); 123 | } 124 | } 125 | 126 | /** 127 | * Helper FN to add class 128 | */ 129 | addLoadingClass(el: any) { 130 | if (typeof this.cfg.btnLoadingClass === 'string') { 131 | el.classList.add(this.cfg.btnLoadingClass); 132 | } 133 | } 134 | 135 | /** 136 | * Helper FN to remove classes 137 | */ 138 | removeLoadingClass(el: any) { 139 | if (typeof this.cfg.btnLoadingClass === 'string') { 140 | el.classList.remove(this.cfg.btnLoadingClass); 141 | } 142 | } 143 | 144 | /** 145 | * Handles everything to be triggered when the button is set 146 | * to loading state. 147 | */ 148 | initLoadingState(btnEl: HTMLElement) { 149 | this.addLoadingClass(btnEl); 150 | this.disableBtn(btnEl); 151 | } 152 | 153 | /** 154 | * Handles everything to be triggered when loading is finished 155 | */ 156 | cancelLoadingStateIfPromiseAndMinDurationDone(btnEl: HTMLElement) { 157 | if ((!this.cfg.minDuration || this.isMinDurationTimeoutDone) && this.isPromiseDone) { 158 | this.removeLoadingClass(btnEl); 159 | this.enableBtn(btnEl); 160 | } 161 | } 162 | 163 | disableBtn(btnEl: HTMLElement) { 164 | if (this.cfg.disableBtn) { 165 | btnEl.setAttribute('disabled', 'disabled'); 166 | } 167 | } 168 | 169 | enableBtn(btnEl: HTMLElement) { 170 | if (this.cfg.disableBtn) { 171 | if (this.isDisabledFromTheOutside) { 172 | btnEl.setAttribute('disabled', 'disabled'); 173 | } else { 174 | btnEl.removeAttribute('disabled'); 175 | } 176 | } 177 | } 178 | 179 | /** 180 | * Initializes a watcher for the promise. Also takes 181 | * this.cfg.minDuration into account if given. 182 | */ 183 | 184 | initPromiseHandler(btnEl: HTMLElement) { 185 | const promise = this.promise; 186 | 187 | // watch promise to resolve or fail 188 | this.isMinDurationTimeoutDone = false; 189 | this.isPromiseDone = false; 190 | 191 | // create timeout if option is set 192 | if (this.cfg.minDuration) { 193 | this.minDurationTimeout = window.setTimeout(() => { 194 | this.isMinDurationTimeoutDone = true; 195 | this.cancelLoadingStateIfPromiseAndMinDurationDone(btnEl); 196 | }, this.cfg.minDuration); 197 | } 198 | 199 | const resolveLoadingState = () => { 200 | this.isPromiseDone = true; 201 | this.cancelLoadingStateIfPromiseAndMinDurationDone(btnEl); 202 | }; 203 | 204 | if (!this.cfg.handleCurrentBtnOnly) { 205 | this.initLoadingState(btnEl); 206 | } 207 | // native Promise doesn't have finally 208 | if (promise.finally) { 209 | promise.finally(resolveLoadingState); 210 | } else { 211 | promise 212 | .then(resolveLoadingState) 213 | .catch(resolveLoadingState); 214 | } 215 | 216 | } 217 | 218 | 219 | /** 220 | * $compile and append the spinner template to the button. 221 | */ 222 | appendSpinnerTpl(btnEl: HTMLElement) { 223 | // TODO add some kind of compilation later on 224 | btnEl.insertAdjacentHTML('beforeend', this.cfg.spinnerTpl as string); 225 | } 226 | 227 | /** 228 | * Limit loading state to show only for the currently clicked button. 229 | * Executed only if this.cfg.handleCurrentBtnOnly is set 230 | */ 231 | @HostListener('click') 232 | handleCurrentBtnOnly() { 233 | if (!this.cfg.handleCurrentBtnOnly) { 234 | return true; // return true for testing 235 | } 236 | 237 | // Click triggers @Input update 238 | // We need to use timeout to wait for @Input to update 239 | window.setTimeout(() => { 240 | // return if something else than a promise is passed 241 | if (!this.promise) { 242 | return; 243 | } 244 | 245 | this.initLoadingState(this.btnEl); 246 | }, 0); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons/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'; 4 | import 'zone.js/dist/async-test.js'; 5 | import 'zone.js/dist/proxy.js'; 6 | import 'zone.js/dist/sync-test'; 7 | import 'zone.js/dist/jasmine-patch'; 8 | 9 | import {getTestBed} from '@angular/core/testing'; 10 | import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing'; 11 | 12 | declare const require: any; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting() 18 | ); 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | 22 | // And load the modules. 23 | context.keys().map(context); 24 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons/src/user-cfg.ts: -------------------------------------------------------------------------------- 1 | import {InjectionToken} from '@angular/core'; 2 | 3 | export const userCfg = new InjectionToken('cfg'); 4 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons/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": false, 16 | "skipTemplateCodegen": true, 17 | "strictMetadataEmit": true, 18 | "fullTemplateTypeCheck": true, 19 | "strictInjectionParameters": true, 20 | "enableResourceInlining": true, 21 | "enableIvy": true, 22 | "allowEmptyCodegenFiles": true, 23 | }, 24 | "exclude": [ 25 | "src/test.ts", 26 | "**/*.spec.ts" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "allowEmptyCodegenFiles": true, 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons/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/angular2-promise-buttons/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../angular2-promise-buttons-demo/tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | false, 6 | "attribute", 7 | "lib", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | false, 12 | "element", 13 | "lib", 14 | "kebab-case" 15 | ], 16 | "variable-name": [ 17 | true, 18 | "allow-pascal-case", 19 | "allow-snake-case", 20 | "ban-keywords", 21 | "check-format", 22 | "allow-leading-underscore" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /projects/angular2-promise-buttons/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@ctrl/tinycolor@^2.6.0": 6 | version "2.6.0" 7 | resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-2.6.0.tgz#c269def5ed1f871913f299a475c01bdb39119eee" 8 | integrity sha512-bvkszNAcbmR2zrjjkaHbTVbEj07Id44HsBWf57mugPcvJNIPaWLqxWV/GUJVJuXXayqFP2X09cZRqKrCy/v10Q== 9 | 10 | -------------------------------------------------------------------------------- /scripts/copy-readme-to-demo.js: -------------------------------------------------------------------------------- 1 | const marked = require('marked'); 2 | const fs = require('fs'); 3 | 4 | const demoIndexPagePath = __dirname + '/../dist/demo/index.html'; 5 | const readmePath = __dirname + '/../README.md'; 6 | const readmeNeedle = '___README_MD_NEEDLE___'; 7 | const readme = fs.readFileSync(readmePath); 8 | const demoIndex = fs.readFileSync(demoIndexPagePath); 9 | 10 | const demoIndexHtml = demoIndex.toString(); 11 | const readmeHtml = marked(readme.toString()); 12 | 13 | const newDemoIndexHtml = demoIndexHtml.replace(readmeNeedle, readmeHtml); 14 | 15 | fs.writeFileSync(demoIndexPagePath, newDemoIndexHtml); 16 | -------------------------------------------------------------------------------- /tsconfig.build-lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "experimentalDecorators": true, 8 | "removeComments": false, 9 | "noImplicitAny": true, 10 | "declaration": true, 11 | "outDir": "./dist", 12 | "stripInternal": true, 13 | "lib": [ 14 | "dom", 15 | "es6" 16 | ] 17 | }, 18 | "include": [ 19 | "projects/angular2-promise-buttons-demo/src/**/*" 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | "**/*.spec.ts" 24 | ], 25 | "files": [ 26 | "./scripts/typings.d.ts", 27 | "./projects/angular2-promise-buttons-demo/src/index.ts" 28 | ], 29 | "angularCompilerOptions": { 30 | "skipTemplateCodegen": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /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": "es2020", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ], 21 | "paths": { 22 | "angular-material-css-variables": [ 23 | "dist/angular-material-css-variables" 24 | ], 25 | "angular-material-css-variables/*": [ 26 | "dist/angular-material-css-variables/*" 27 | ], 28 | "material-css-vars": [ 29 | "dist/material-css-vars" 30 | ], 31 | "material-css-vars/*": [ 32 | "dist/material-css-vars/*" 33 | ] 34 | } 35 | }, 36 | "angularCompilerOptions": { 37 | "fullTemplateTypeCheck": true, 38 | "strictInjectionParameters": true 39 | } 40 | } -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const wallabyWebpack = require('wallaby-webpack'); 4 | const path = require('path'); 5 | 6 | const compilerOptions = Object.assign( 7 | require('./tsconfig.json').compilerOptions); 8 | 9 | // don't generate the declaration files (.d.ts) 10 | compilerOptions.declaration = false; 11 | 12 | module.exports = function (wallaby) { 13 | 14 | const webpackPostprocessor = wallabyWebpack({ 15 | entryPatterns: [ 16 | 'scripts/wallabyTest.js', 17 | 'src/**/*spec.js' 18 | ], 19 | 20 | module: { 21 | loaders: [ 22 | { test: /\.css$/, loader: 'raw-loader' }, 23 | { test: /\.html$/, loader: 'raw-loader' }, 24 | { test: /\.js$/, loader: 'angular2-template-loader', exclude: /node_modules/ }, 25 | { test: /\.json$/, loader: 'json-loader' }, 26 | { test: /\.styl$/, loaders: ['raw-loader', 'stylus-loader'] }, 27 | { test: /\.less$/, loaders: ['raw-loader', 'less-loader'] }, 28 | { test: /\.scss$|\.sass$/, loaders: ['raw-loader', 'sass-loader'] }, 29 | { test: /\.(jpg|png)$/, loader: 'url-loader?limit=128000' } 30 | ] 31 | }, 32 | 33 | resolve: { 34 | modules: [ 35 | path.join(wallaby.projectCacheDir, 'demo/src'), 36 | path.join(wallaby.projectCacheDir, 'src') 37 | ] 38 | } 39 | }); 40 | 41 | return { 42 | files: [ 43 | { pattern: 'src/**/*.ts', load: false }, 44 | { pattern: 'src/**/*.d.ts', ignore: true }, 45 | { pattern: 'src/**/*.css', load: false }, 46 | { pattern: 'src/**/*.less', load: false }, 47 | { pattern: 'src/**/*.scss', load: false }, 48 | { pattern: 'src/**/*.sass', load: false }, 49 | { pattern: 'src/**/*.styl', load: false }, 50 | { pattern: 'src/**/*.html', load: false }, 51 | { pattern: 'src/**/*.json', load: false }, 52 | { pattern: 'scripts/*.ts', load: false }, 53 | { pattern: 'src/**/*spec.ts', ignore: true }, 54 | { pattern: 'demo/src/**/*.ts', load: false }, 55 | { pattern: 'demo/src/**/*.d.ts', ignore: true }, 56 | { pattern: 'demo/src/**/*.css', load: false }, 57 | { pattern: 'demo/src/**/*.less', load: false }, 58 | { pattern: 'demo/src/**/*.scss', load: false }, 59 | { pattern: 'demo/src/**/*.sass', load: false }, 60 | { pattern: 'demo/src/**/*.styl', load: false }, 61 | { pattern: 'demo/src/**/*.html', load: false }, 62 | { pattern: 'demo/src/**/*.json', load: false }, 63 | { pattern: 'demo/src/**/*spec.ts', ignore: true } 64 | ], 65 | 66 | tests: [ 67 | { pattern: 'src/**/*spec.ts', load: false } 68 | ], 69 | 70 | testFramework: 'jasmine', 71 | preprocessors: { 72 | './scripts/test.ts': ['@angular/cli'] 73 | }, 74 | compilers: { 75 | '**/*.ts': wallaby.compilers.typeScript(compilerOptions) 76 | }, 77 | 78 | env: { 79 | kind: 'electron' 80 | }, 81 | 82 | postprocessor: webpackPostprocessor, 83 | 84 | setup: function () { 85 | window.__moduleBundler.loadTests(); 86 | }, 87 | 88 | debug: true 89 | }; 90 | }; 91 | --------------------------------------------------------------------------------