├── .gitignore ├── .npmignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── MIGRATION_GUIDE.md ├── README.md ├── angular.json ├── config ├── helpers.js ├── karma.conf.js ├── spec-bundle.js ├── testing-utils.d.ts ├── testing-utils.ts └── webpack.test.js ├── demo ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.e2e.json ├── package.json ├── src │ ├── .browserslistrc │ ├── app │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ └── dialogs │ │ │ ├── custom-modal.component.ts │ │ │ └── dynamic-modal.component.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── karma.conf.js │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ ├── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json ├── tsconfig.base.json ├── tsconfig.json ├── tslint.json └── yarn.lock ├── karma.conf.js ├── ng-package.json ├── package.json ├── src ├── modal-dialog-instance.service.ts ├── modal-dialog.component.ts ├── modal-dialog.interface.ts ├── modal-dialog.module.ts ├── modal-dialog.service.ts ├── public-api.ts └── simple-modal.component.ts ├── tests ├── modal-dialog-instance.service.spec.ts ├── modal-dialog.component.spec.ts ├── modal-dialog.service.spec.ts └── simple-modal.component.spec.ts ├── tsconfig.json ├── tsconfig.lib.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | ############ 2 | ## Project 3 | ############ 4 | 5 | bundles/ 6 | node_modules/ 7 | coverage/ 8 | 9 | ################# 10 | ## Compiled 11 | ################# 12 | 13 | *.map 14 | *.metadata.json 15 | *.d.ts 16 | *.js 17 | !config/* 18 | !karma.conf.js 19 | !protractor.conf.js 20 | !demo/src/typings.d.ts 21 | 22 | ################# 23 | ## Misc 24 | ################# 25 | 26 | nbproject 27 | manifest.mf 28 | build.xml 29 | npm-debug.log 30 | 31 | ################# 32 | ## IDEs 33 | ################# 34 | 35 | .idea 36 | .project 37 | .settings 38 | .vscode 39 | 40 | ############ 41 | ## Windows 42 | ############ 43 | 44 | Thumbs.db 45 | Desktop.ini 46 | 47 | ############ 48 | ## Mac 49 | ############ 50 | 51 | .DS_Store 52 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Misc 3 | ################# 4 | nbproject 5 | manifest.mf 6 | build.xml 7 | 8 | ################# 9 | ## Project 10 | ################# 11 | 12 | node_modules/* 13 | npm-debug.log 14 | *.ts 15 | !*.d.ts 16 | tests 17 | coverage 18 | config 19 | karma.conf.js 20 | tsconfig.json 21 | tslint.json 22 | package-lock.json 23 | CODE_OF_CONDUCT.md 24 | .* 25 | yarn.lock 26 | 27 | ############ 28 | ## Windows 29 | ############ 30 | 31 | # Windows image file caches 32 | Thumbs.db 33 | 34 | # Folder config file 35 | Desktop.ini 36 | 37 | ############ 38 | ## Mac 39 | ############ 40 | 41 | # Mac crap 42 | .DS_Store 43 | 44 | ############ 45 | ## Demos 46 | ############ 47 | 48 | demo/* 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | script: 7 | - cd demo 8 | - yarn 9 | - ng build --prod --base-href "https://greentube.github.io/ngx-modal/demo/" 10 | - ng deploy --no-silent --repo=https://GH_TOKEN@github.com/greentube/ngx-modal.git --name="meeroslav" --email=missing.manual@gmail.com 11 | 12 | notifications: 13 | email: false 14 | node_js: 15 | - "10" 16 | before_install: 17 | - export CHROME_BIN=chromium-browser 18 | - npm i -g npm@^3 19 | before_script: 20 | - npm prune 21 | - export DISPLAY=:99.0 22 | after_success: 23 | - npm run semantic-release 24 | branches: 25 | except: 26 | - "/^v\\d+\\.\\d+\\.\\d+$/" 27 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at meeroslav@yahoo.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Greentube I.E.S. GmbH 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 | -------------------------------------------------------------------------------- /MIGRATION_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Migration guide from 1.x.x to 2.x.x 2 | 3 | The 2.0.0 release is introducing a breaking change in method signature, interfaces and default css classes. 4 | 5 | This guide is provided to make the transition as painless as possible. 6 | 7 | Steps to migrate your code are: 8 | - update the npm package 9 | - change code according to changes 10 | 11 | 1. Update in your package.json `ngx-modal-dialog` to the latest 2.x.x version 12 | ([check the current release here](https://github.com/Greentube/ngx-modal/releases)) 13 | 2. The `dialogInit` method had slightly changed: 14 | ```ts 15 | // old signature 16 | dialogInit(reference: ComponentRef, options?: IModalDialogOptions) { 17 | } 18 | 19 | // new signature 20 | dialogInit(reference: ComponentRef, options: Partial> = {}) { 21 | } 22 | ``` 23 | You can now provide your custom type as a parameter in generic `IModalDialogOptions` interface to enforce 24 | types strictness. 25 | 3. Dialog is now using only very basic styles (designed to work on top of Bootstrap 4) so you might need to modify your styles if you were heavily 26 | depending on the existing styles. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngx modal dialog 2 | [![Build Status](https://travis-ci.org/Greentube/ngx-modal.svg?branch=master)](https://travis-ci.org/Greentube/ngx-modal) 3 | [![npm version](https://img.shields.io/npm/v/ngx-modal-dialog.svg)](https://www.npmjs.com/package/ngx-modal-dialog) 4 | > Dynamic modal dialog for Angular that does not sit on DOM waiting to be triggered, but rather gets injected upon need. 5 | 6 | Made with Bootstrap 4 styles in mind, but configurable to any framework or custom set of styles. 7 | Simple demo can be found [here](https://greentube.github.io/ngx-modal/demo). 8 | 9 | > This documentation is for version 4.x.x and Angular 10+. If you are using older version of Angular please use [3.x version for v6-v8](https://github.com/Greentube/ngx-modal/tree/v3.x.x) or [2.x version for v2-v5](https://github.com/Greentube/ngx-modal/tree/v2.x.x). 10 | 11 | # Table of contents: 12 | - [Installation](#installation) 13 | - [How it works](#how-it-works) 14 | - [Styles and visuals](#styles-and-visuals) 15 | - [Usage](#usage) 16 | - [API](#api) 17 | - [ModalDialogService](#modaldialogservice) 18 | - [IModalDialog](#imodaldialog) 19 | - [IModalDialogOptions](#imodaldialogoptions) 20 | - [IModalDialogButton](#imodaldialogbutton) 21 | - [IModalDialogSettings](#imodaldialogsettings) 22 | - [License](#license) 23 | 24 | ## Installation 25 | 26 | ``` 27 | npm install --save ngx-modal-dialog 28 | ``` 29 | ## How it works 30 | Modal dialog uses `ComponentFactoryResolver` to inject the given child component to the dialog. 31 | [ModalDialogService](#modaldialogservice) makes sure that only one instance of a modal dialog is opened at a time. 32 | With [IModalDialogOptions](#imodaldialogoptions) you can define which component will be rendered inside the dialog and configure it based on your needs. 33 | 34 | You can further use action buttons to control modal dialog from external component or child component. If action performed on button click is successful, modal dialog will close. Otherwise it will alert user. 35 | 36 | ## Styles and visuals 37 | 38 | `Ngx-modal-dialog` is intended to be used with Bootstrap 4, however you can apply your custom styles from your desired UI framework by providing class names in [IModalDialogSettings](#imodaldialogsettings). 39 | 40 | ## Usage 41 | 42 | 1. Include the `ngx-modal` module in your application at any place. The recommended way is to add `forRoot` initialization in the main app module. 43 | ```ts 44 | import { BrowserModule } from '@angular/platform-browser'; 45 | import { ModalDialogModule } from 'ngx-modal-dialog'; 46 | 47 | @NgModule({ 48 | imports: [ 49 | BrowserModule, 50 | ModalDialogModule.forRoot() 51 | ], 52 | bootstrap: [AppComponent] 53 | }) 54 | export class AppModule { } 55 | ``` 56 | 2. Create a custom component that implements `IModalDialog` or use the `SimpleModalDialog` as a child component. 57 | 58 | Custom component should be inserted into both `declarations` and `entryComponents` in the NgModule they are part of. `entryComponents` has to be used since component is dynamically inserted onto the page and Angular is not aware of it. 59 | 60 | 3. Inject the `ModalDialogService` where you want to open the dialog. Call `openDialog` passing parent `ViewContainerRef` and partial `IModalDialogOptions` object: 61 | ```ts 62 | constructor(modalService: ModalDialogService, viewRef: ViewContainerRef) { } 63 | 64 | openNewDialog() { 65 | this.modalService.openDialog(this.viewRef, { 66 | title: 'Some modal title', 67 | childComponent: SimpleModalComponent 68 | }); 69 | } 70 | ``` 71 | 4. Arbitrary define `actionButtons` in modal dialog options or child component to control modal dialog. 72 | 73 | ```ts 74 | class MyModalComponent implements IModalDialog { 75 | actionButtons: IModalDialogButton[]; 76 | 77 | constructor() { 78 | this.actionButtons = [ 79 | { text: 'Close' }, // no special processing here 80 | { text: 'I will always close', onAction: () => true }, 81 | { text: 'I never close', onAction: () => false } 82 | ]; 83 | } 84 | 85 | dialogInit(reference: ComponentRef, options: Partial>) { 86 | // no processing needed 87 | } 88 | } 89 | ``` 90 | 91 | Action button can be of two types: 92 | - with return value 93 | Used for controlling the dialog life. 94 | If value is `truthful` (true, successful Promise or Observable) than it will close the dialog 95 | If value is `falsy` (false, rejected Promise or failed Observable) it will trigger alert style and not close the dialog. 96 | - without return value 97 | Has no direct effect on dialog. Can be used to trigger some arbitrary functionality (e.g. copy values to clipboard) 98 | 99 | ## API 100 | 101 | ### ModalDialogService 102 | #### Methods: 103 | - `openDialog(target: ViewContainerRef, options: Partial> = {})`: Closes existing and opens a new modal dialog according to IModalDialogOptions. 104 | `T` represents a type of options `data` field. If you don't care about strong typing just pass `any`. 105 | 106 | ### IModalDialog 107 | Every component that is used as modal dialog must implement `IModalDialog`. 108 | #### Methods: 109 | - `dialogInit(reference: ComponentRef, options: Partial>) => void` 110 | Mandatory: `true` 111 | Default: - 112 | This method is called after initialization of child component. Purpose of the method is to pass necessary information from outer scope to child component. 113 | #### Properties: 114 | - `actionButtons` 115 | Mandatory: `false` 116 | Default: - 117 | Type: `string` 118 | Modal heading text 119 | 120 | ### IModalDialogOptions 121 | #### Interface: 122 | ```ts 123 | interface IModalDialogOptions { 124 | title: string; 125 | childComponent: IModalDialog; 126 | onClose: ModalDialogOnAction; 127 | actionButtons: IModalDialogButton[]; 128 | data: T; 129 | placeOnTop: boolean; 130 | settings: IModalDialogSettings; 131 | closeDialogSubject: Subject; 132 | } 133 | ``` 134 | This is generic interface, where `T` is arbitrary type of `data` section. 135 | #### Interface details: 136 | - title: `string` 137 | Modal heading text 138 | 139 | - childComponent: `any` 140 | Component type that will be rendered as a content of modal dialog. Component must implement `IModalDialog` interface. 141 | 142 | - onClose(): `ModalDialogOnAction` 143 | Function to be called on close button click. In case of Promise and Observable, modal dialog will not close unless successful resolve happens. In case of boolean, modal dialog will close only if result is `truthful`. 144 | 145 | - actionButtons: `Array` 146 | Footer action buttons for control of modal dialog. See [IModalDialogButton](#imodaldialogbutton). 147 | Action buttons defined in child component have priority over action buttons defined via options. 148 | Action buttons close the modal dialog upon successful operation. 149 | 150 | - data: `T` 151 | Arbitrary data that will be passed to child component via `dialogInit` method. 152 | 153 | - placeOnTop: `boolean` 154 | Flag stating whether opening the modal dialog should close all the other modal dialogs, or modal should be rendered on top of existing ones. 155 | 156 | 157 | - settings: `IModalDialogSettings` 158 | Additional settings for granular configuration of modal dialog. See [IModalDialogSettings](#imodaldialogsettings). 159 | 160 | - closeDialogSubject:`Subject` 161 | Custom modal closing subject. Can be used to manually trigger modal dialog close from within the child component. 162 | 163 | ### IModalDialogButton 164 | #### Interface: 165 | ```ts 166 | interface IModalDialogButton { 167 | text: string; 168 | buttonClass?: string; 169 | onAction?: ModalDialogOnAction; 170 | } 171 | ``` 172 | #### Interface details: 173 | - text: `string` 174 | Caption/text on the button 175 | - buttonClass: `string` 176 | Default: `btn btn-primary` 177 | Class name of button 178 | - onAction(): `Promise | Observable | boolean` 179 | Function to be called on button click. In case of Promise and Observable, modal dialog will not close unless successful resolve happens. In case of boolean, modal dialog will close only if result is `truthful`. 180 | #### ModalDialogOnAction type 181 | ```ts 182 | type ModalDialogOnAction = () => Promise | Observable | boolean | void; 183 | ``` 184 | Function returning Promise, Observable, boolean or no value. Modal dialog will close automatically if return of action is: 185 | * Promise, once promise gets resolved 186 | * Observable, once observable successfully finishes 187 | * boolean and value is `true` 188 | 189 | Action button will initiate alert behavior if return is: 190 | * Promise, once promise gets rejected 191 | * Observable, once observable fails or throws error 192 | * boolean and value is `false` 193 | 194 | If action button returns `void`, there are no side effects. 195 | 196 | ### IModalDialogSettings 197 | #### Interface 198 | ```ts 199 | interface IModalDialogSettings { 200 | overlayClass: string; 201 | overlayAnimationTriggerClass: string; 202 | modalClass: string; 203 | modalAnimationTriggerClass: string; 204 | contentClass: string; 205 | headerClass: string; 206 | headerTitleClass: string; 207 | closeButtonClass: string; 208 | closeButtonTitle: string; 209 | bodyClass: string; 210 | footerClass: string; 211 | alertClass: string; 212 | alertDuration: number; 213 | buttonClass: string; 214 | notifyWithAlert: boolean; 215 | } 216 | ``` 217 | 218 | #### Interface details: 219 | - overlayClass: `string` 220 | Default: `modal-backdrop fade show` 221 | Style of the backdrop overlay layer 222 | - overlayAnimationTriggerClass: `string` 223 | Default: `show` 224 | Class that triggers the initial/ending animation of modal overlay 225 | - modalClass: `string` 226 | Default: `modal fade ngx-modal` 227 | Style of modal wrapper 228 | - modalAnimationTriggerClass: `string` 229 | Default: `show` 230 | Class that triggers the initial/ending animation of modal wrapper 231 | - modalDialogClass: `string` 232 | Default: `modal-dialog modal-dialog-centered` 233 | Style of modal dialog 234 | - contentClass: `string` 235 | Default: `modal-content` 236 | Modal dialog inner content class 237 | - headerClass: `string` 238 | Default: `modal-header` 239 | Modal dialog header class 240 | - headerTitleClass: `string` 241 | Default: `modal-title` 242 | Modal dialog header title class 243 | - closeButtonClass: `string` 244 | Default: `close glyphicon glyphicon-remove` 245 | Modal dialog header close button class 246 | - closeButtonTitle: `string` 247 | Default: `CLOSE` 248 | Close button title 249 | - bodyClass: `string` 250 | Default: `modal-body` 251 | Modal dialog body class 252 | - footerClass: `string` 253 | Default: `modal-footer` 254 | Modal dialog footer class 255 | - alertClass: `string` 256 | Default: `ngx-modal-shake` 257 | Style to be appended to dialog once alert happens 258 | - alertDuration: `number` 259 | Default: `250` 260 | Duration of alert animation 261 | - buttonClass: `string` 262 | Default: `btn btn-primary` 263 | Style of footer action buttons 264 | - notifyWithAlert: `number` 265 | Default: `true` 266 | Define whether modal should alert user when action fails 267 | 268 | ## License 269 | Licensed under MIT 270 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "projects": { 5 | "ngx-modal-dialog": { 6 | "projectType": "library", 7 | "root": "./", 8 | "sourceRoot": "./src", 9 | "prefix": "lib", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-ng-packagr:build", 13 | "options": { 14 | "tsConfig": "./tsconfig.lib.json", 15 | "project": "./ng-package.json" 16 | }, 17 | "configurations": { 18 | "production": { 19 | "tsConfig": "./tsconfig.lib.json" 20 | } 21 | } 22 | } 23 | } 24 | }}, 25 | "defaultProject": "ngx-modal-dialog" 26 | } 27 | -------------------------------------------------------------------------------- /config/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * taken from angular2-webpack-starter 3 | */ 4 | var path = require('path'); 5 | 6 | // Helper functions 7 | var ROOT = path.resolve(__dirname, '..'); 8 | 9 | function hasProcessFlag(flag) { 10 | return process.argv.join('').indexOf(flag) > -1; 11 | } 12 | 13 | function isWebpackDevServer() { 14 | return process.argv[1] && !! (/webpack-dev-server$/.exec(process.argv[1])); 15 | } 16 | 17 | function root(args) { 18 | args = Array.prototype.slice.call(arguments, 0); 19 | return path.join.apply(path, [ROOT].concat(args)); 20 | } 21 | 22 | function checkNodeImport(context, request, cb) { 23 | if (!path.isAbsolute(request) && request.charAt(0) !== '.') { 24 | cb(null, 'commonjs ' + request); return; 25 | } 26 | cb(); 27 | } 28 | 29 | exports.hasProcessFlag = hasProcessFlag; 30 | exports.isWebpackDevServer = isWebpackDevServer; 31 | exports.root = root; 32 | exports.checkNodeImport = checkNodeImport; -------------------------------------------------------------------------------- /config/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | const testWebpackConfig = require('./webpack.test.js'); 3 | const configuration = { 4 | basePath: '', 5 | 6 | client:{ 7 | clearContext: false // leave Jasmine Spec Runner output visible in browser 8 | }, 9 | 10 | frameworks: ['jasmine'], 11 | 12 | plugins: [ 13 | require('karma-jasmine'), 14 | require('karma-chrome-launcher'), 15 | require('karma-sourcemap-loader'), 16 | require('karma-webpack'), 17 | require('karma-coverage'), 18 | require('karma-webpack'), 19 | require('karma-remap-coverage'), 20 | require('karma-mocha-reporter') 21 | ], 22 | exclude: [], 23 | 24 | files: [{ pattern: './config/spec-bundle.js', watched: false }], 25 | 26 | preprocessors: { './config/spec-bundle.js': ['coverage', 'webpack', 'sourcemap'] }, 27 | 28 | webpack: testWebpackConfig, 29 | 30 | webpackMiddleware: { 31 | logLevel: 'warn', 32 | stats: { 33 | chunks: false 34 | } 35 | }, 36 | 37 | reporters: ['mocha', 'coverage', 'remap-coverage'], 38 | 39 | coverageReporter: { 40 | type: 'in-memory' 41 | }, 42 | 43 | remapCoverageReporter: { 44 | 'text-summary': null, 45 | json: './coverage/coverage.json', 46 | html: './coverage/html' 47 | }, 48 | 49 | mochaReporter: { 50 | ignoreSkipped: true 51 | }, 52 | webpackServer: { noInfo: true }, 53 | port: 9876, 54 | colors: true, 55 | logLevel: config.LOG_WARN, 56 | autoWatch: false, 57 | browsers: [ 'ChromeHeadless' ], 58 | 59 | customLaunchers: { 60 | ChromeTravisCi: { 61 | base: 'ChromeHeadless', 62 | flags: ['--no-sandbox', '--disable-gpu'] 63 | } 64 | }, 65 | singleRun: true 66 | }; 67 | 68 | if (process.env.TRAVIS) { 69 | configuration.browsers = ['ChromeTravisCi']; 70 | configuration.reporters = ['mocha']; 71 | } 72 | 73 | config.set(configuration); 74 | }; 75 | -------------------------------------------------------------------------------- /config/spec-bundle.js: -------------------------------------------------------------------------------- 1 | Error.stackTraceLimit = Infinity; 2 | 3 | require('core-js/features/reflect'); 4 | 5 | // Typescript emit helpers polyfill 6 | require('ts-helpers'); 7 | require('./testing-utils'); 8 | 9 | require('zone.js/dist/zone'); 10 | require('zone.js/dist/long-stack-trace-zone'); 11 | require('zone.js/dist/proxy.js'); 12 | require('zone.js/dist/sync-test'); 13 | require('zone.js/dist/jasmine-patch'); 14 | require('zone.js/dist/async-test'); 15 | require('zone.js/dist/fake-async-test'); 16 | 17 | let testing = require('@angular/core/testing'); 18 | let browser = require('@angular/platform-browser-dynamic/testing'); 19 | 20 | testing.getTestBed().initTestEnvironment( 21 | browser.BrowserDynamicTestingModule, 22 | browser.platformBrowserDynamicTesting() 23 | ); 24 | // Then we find all the tests. 25 | const context = require.context('../tests', true, /\.spec\.ts$/); 26 | // And load the modules. 27 | const modules = context.keys().map(context); 28 | -------------------------------------------------------------------------------- /config/testing-utils.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /config/testing-utils.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* 4 | Temporary file for referencing the TypeScript defs for Jasmine + some potentially 5 | utils for testing. Will change/adjust this once I find a better way of doing 6 | */ 7 | 8 | beforeEach(() => { 9 | jasmine.addMatchers({ 10 | 11 | toHaveText: function() { 12 | return { 13 | compare: function(actual: any, expectedText: any) { 14 | var actualText = actual.textContent; 15 | return { 16 | pass: actualText === expectedText, 17 | get message() { 18 | return 'Expected ' + actualText + ' to equal ' + expectedText; 19 | } 20 | }; 21 | } 22 | }; 23 | }, 24 | 25 | toContainText: function() { 26 | return { 27 | compare: function(actual: any, expectedText: any) { 28 | var actualText = actual.textContent; 29 | return { 30 | pass: actualText.indexOf(expectedText) > -1, 31 | get message() { 32 | return 'Expected ' + actualText + ' to contain ' + expectedText; 33 | } 34 | }; 35 | } 36 | }; 37 | }, 38 | 39 | toBeAnInstanceOf: function () { 40 | return { 41 | compare: function (actual: any, expectedClass: any) { 42 | const pass = typeof actual === 'object' && actual instanceof expectedClass; 43 | return { 44 | pass: pass, 45 | get message() { 46 | return 'Expected ' + actual + ' to be an instance of ' + expectedClass; 47 | } 48 | }; 49 | } 50 | }; 51 | }, 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /config/webpack.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from angular2-webpack-starter 3 | */ 4 | 5 | const helpers = require('./helpers'); 6 | const webpack = require('webpack'); 7 | const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); 8 | 9 | module.exports = { 10 | /** 11 | * Source map for Karma from the help of karma-sourcemap-loader & karma-webpack 12 | * 13 | * Do not change, leave as is or it wont work. 14 | * See: https://github.com/webpack/karma-webpack#source-maps 15 | */ 16 | devtool: 'inline-source-map', 17 | 18 | resolve: { 19 | extensions: ['.ts', '.js'], 20 | modules: [helpers.root('src'), 'node_modules'] 21 | }, 22 | 23 | module: { 24 | rules: [ 25 | { 26 | enforce: 'pre', 27 | test: /\.ts$/, 28 | loader: 'tslint-loader', 29 | exclude: [helpers.root('node_modules')] 30 | }, 31 | { 32 | enforce: 'pre', 33 | test: /\.js$/, 34 | loader: 'source-map-loader', 35 | exclude: [ 36 | // these packages have problems with their sourcemaps 37 | helpers.root('node_modules/rxjs'), 38 | helpers.root('node_modules/@angular') 39 | ] 40 | }, 41 | { 42 | test: /\.ts$/, 43 | loader: 'awesome-typescript-loader', 44 | query: { 45 | // use inline sourcemaps for "karma-remap-coverage" reporter 46 | sourceMap: false, 47 | inlineSourceMap: true, 48 | module: "commonjs", 49 | removeComments: true 50 | }, 51 | exclude: [/\.e2e\.ts$/] 52 | }, 53 | { 54 | enforce: 'post', 55 | test: /\.(js|ts)$/, 56 | loader: 'istanbul-instrumenter-loader', 57 | include: helpers.root('src'), 58 | exclude: [/\.spec\.ts$/, /node_modules/] 59 | } 60 | ] 61 | }, 62 | 63 | plugins: [ 64 | new webpack.ContextReplacementPlugin( 65 | /angular(\\|\/)core(\\|\/)@angular/, 66 | helpers.root('./src') 67 | ), 68 | new LoaderOptionsPlugin({ 69 | debug: true, 70 | options: { 71 | tslint: { 72 | emitErrors: false, 73 | failOnHint: false, 74 | resourcePath: 'src' 75 | } 76 | } 77 | }) 78 | ] 79 | }; 80 | -------------------------------------------------------------------------------- /demo/.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 | -------------------------------------------------------------------------------- /demo/.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 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Demo 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.0.8. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /demo/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "demo": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "aot": true, 17 | "outputPath": "dist/demo", 18 | "index": "src/index.html", 19 | "main": "src/main.ts", 20 | "polyfills": "src/polyfills.ts", 21 | "tsConfig": "src/tsconfig.app.json", 22 | "assets": [ 23 | "src/favicon.ico", 24 | "src/assets" 25 | ], 26 | "styles": [ 27 | "src/styles.scss" 28 | ], 29 | "scripts": [] 30 | }, 31 | "configurations": { 32 | "production": { 33 | "budgets": [ 34 | { 35 | "type": "anyComponentStyle", 36 | "maximumWarning": "6kb" 37 | } 38 | ], 39 | "fileReplacements": [ 40 | { 41 | "replace": "src/environments/environment.ts", 42 | "with": "src/environments/environment.prod.ts" 43 | } 44 | ], 45 | "optimization": true, 46 | "outputHashing": "all", 47 | "sourceMap": false, 48 | "extractCss": true, 49 | "namedChunks": false, 50 | "aot": true, 51 | "extractLicenses": true, 52 | "vendorChunk": false, 53 | "buildOptimizer": true 54 | } 55 | } 56 | }, 57 | "serve": { 58 | "builder": "@angular-devkit/build-angular:dev-server", 59 | "options": { 60 | "browserTarget": "demo:build" 61 | }, 62 | "configurations": { 63 | "production": { 64 | "browserTarget": "demo:build:production" 65 | } 66 | } 67 | }, 68 | "extract-i18n": { 69 | "builder": "@angular-devkit/build-angular:extract-i18n", 70 | "options": { 71 | "browserTarget": "demo:build" 72 | } 73 | }, 74 | "test": { 75 | "builder": "@angular-devkit/build-angular:karma", 76 | "options": { 77 | "main": "src/test.ts", 78 | "polyfills": "src/polyfills.ts", 79 | "tsConfig": "src/tsconfig.spec.json", 80 | "karmaConfig": "src/karma.conf.js", 81 | "styles": [ 82 | "src/styles.scss" 83 | ], 84 | "scripts": [], 85 | "assets": [ 86 | "src/favicon.ico", 87 | "src/assets" 88 | ] 89 | } 90 | }, 91 | "lint": { 92 | "builder": "@angular-devkit/build-angular:tslint", 93 | "options": { 94 | "tsConfig": [ 95 | "src/tsconfig.app.json", 96 | "src/tsconfig.spec.json" 97 | ], 98 | "exclude": [ 99 | "**/node_modules/**" 100 | ] 101 | } 102 | }, 103 | "deploy": { 104 | "builder": "angular-cli-ghpages:deploy", 105 | "options": {} 106 | } 107 | } 108 | }, 109 | "demo-e2e": { 110 | "root": "e2e/", 111 | "projectType": "application", 112 | "architect": { 113 | "e2e": { 114 | "builder": "@angular-devkit/build-angular:protractor", 115 | "options": { 116 | "protractorConfig": "e2e/protractor.conf.js", 117 | "devServerTarget": "demo:serve" 118 | }, 119 | "configurations": { 120 | "production": { 121 | "devServerTarget": "demo:serve:production" 122 | } 123 | } 124 | }, 125 | "lint": { 126 | "builder": "@angular-devkit/build-angular:tslint", 127 | "options": { 128 | "tsConfig": "e2e/tsconfig.e2e.json", 129 | "exclude": [ 130 | "**/node_modules/**" 131 | ] 132 | } 133 | } 134 | } 135 | } 136 | }, 137 | "defaultProject": "demo" 138 | } -------------------------------------------------------------------------------- /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 | './src/**/*.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 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /demo/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to demo!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /demo/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demo/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "11.2.5", 15 | "@angular/common": "11.2.5", 16 | "@angular/compiler": "11.2.5", 17 | "@angular/core": "11.2.5", 18 | "@angular/forms": "11.2.5", 19 | "@angular/platform-browser": "11.2.5", 20 | "@angular/platform-browser-dynamic": "11.2.5", 21 | "@angular/router": "11.2.5", 22 | "bootstrap": "~4.3.1", 23 | "core-js": "^2.5.4", 24 | "ngx-modal-dialog": "file:../bundles", 25 | "rxjs": "6.6.6", 26 | "tslib": "2.1.0", 27 | "zone.js": "0.11.4" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "0.1102.4", 31 | "@angular/cli": "11.2.4", 32 | "@angular/compiler-cli": "11.2.5", 33 | "@types/jasmine": "~2.8.6", 34 | "@types/jasminewd2": "~2.0.3", 35 | "@types/node": "^12.11.1", 36 | "angular-cli-ghpages": "1.0.0-rc.1", 37 | "codelyzer": "6.0.1", 38 | "jasmine-core": "~3.5.0", 39 | "jasmine-spec-reporter": "~5.0.0", 40 | "karma": "~5.0.0", 41 | "karma-chrome-launcher": "~3.1.0", 42 | "karma-coverage-istanbul-reporter": "~3.0.2", 43 | "karma-jasmine": "~3.3.0", 44 | "karma-jasmine-html-reporter": "^1.5.0", 45 | "protractor": "~7.0.0", 46 | "ts-node": "~5.0.1", 47 | "tslint": "~6.1.0", 48 | "typescript": "4.1.5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /demo/src/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | IE 9-11 10 | -------------------------------------------------------------------------------- /demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 4 |
5 |
6 |

7 | Demo showcase of ngx-modal-dialog component 8 |

9 |
10 |
11 | 12 |
13 |
14 |
15 |
16 | 18 |
19 |
20 |
21 |
22 | 23 |
24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 | 38 |
39 |
40 |
41 |
42 | -------------------------------------------------------------------------------- /demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewContainerRef } from '@angular/core'; 2 | import { ModalDialogService, SimpleModalComponent } from 'ngx-modal-dialog'; 3 | import { CustomModalComponent } from './dialogs/custom-modal.component'; 4 | import { DynamicModalComponent } from './dialogs/dynamic-modal.component'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | templateUrl: './app.component.html' 9 | }) 10 | export class AppComponent { 11 | constructor(private modalDialogService: ModalDialogService, private viewContainer: ViewContainerRef) {} 12 | 13 | openSimpleModal() { 14 | this.modalDialogService.openDialog(this.viewContainer, { 15 | title: 'Simple dialog', 16 | childComponent: SimpleModalComponent, 17 | settings: { 18 | closeButtonClass: 'close theme-icon-close' 19 | }, 20 | data: { 21 | text: 'Some text content' 22 | } 23 | }); 24 | } 25 | 26 | openSimpleModalWithCallback() { 27 | this.modalDialogService.openDialog(this.viewContainer, { 28 | title: 'Dialog with delayed closing', 29 | childComponent: SimpleModalComponent, 30 | data: { 31 | text: 'Some text content. It will close after 1 sec.' 32 | }, 33 | settings: { 34 | closeButtonClass: 'close theme-icon-close' 35 | }, 36 | onClose: () => new Promise((resolve: any) => { 37 | setTimeout(() => { 38 | resolve(); 39 | }, 1000); 40 | }) 41 | }); 42 | } 43 | 44 | openPromptModal() { 45 | this.modalDialogService.openDialog(this.viewContainer, { 46 | title: 'Dialog with action buttons', 47 | childComponent: SimpleModalComponent, 48 | data: { 49 | text: 'Not so simple modal dialog. Do you agree?\n(It will close on Yes, fail on No and do nothing on Site effect)' 50 | }, 51 | settings: { 52 | closeButtonClass: 'close theme-icon-close' 53 | }, 54 | actionButtons: [ 55 | { 56 | text: 'Yes, close me!', 57 | buttonClass: 'btn btn-success', 58 | onAction: () => new Promise((resolve: any) => { 59 | setTimeout(() => { 60 | resolve(); 61 | }, 20); 62 | }) 63 | }, 64 | { 65 | text: 'Side effect', 66 | buttonClass: 'btn btn-info', 67 | onAction: () => { 68 | alert('As you can see, I will not close this dialog'); 69 | } 70 | }, 71 | { 72 | text: 'No, fail closing!', 73 | buttonClass: 'btn btn-danger', 74 | onAction: () => new Promise((resolve: any, reject: any) => { 75 | setTimeout(() => { 76 | reject(); 77 | }, 20); 78 | }) 79 | } 80 | ] 81 | }); 82 | } 83 | 84 | openCustomModal() { 85 | this.modalDialogService.openDialog(this.viewContainer, { 86 | title: 'Custom child component', 87 | childComponent: CustomModalComponent, 88 | settings: { 89 | closeButtonClass: 'close theme-icon-close' 90 | }, 91 | data: 'Hey, we are data passed from the parent!' 92 | }); 93 | } 94 | 95 | openDynamicModal() { 96 | this.modalDialogService.openDialog(this.viewContainer, { 97 | title: 'Dynamic child component', 98 | childComponent: DynamicModalComponent, 99 | settings: { 100 | closeButtonClass: 'close theme-icon-close' 101 | } 102 | }); 103 | } 104 | 105 | openMultipleModal() { 106 | this.modalDialogService.openDialog(this.viewContainer, { 107 | title: 'Dialog 1', 108 | childComponent: SimpleModalComponent, 109 | settings: { closeButtonClass: 'close theme-icon-close' }, 110 | placeOnTop: true, 111 | data: { text: ` 112 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, 113 | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 114 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 115 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 116 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.` } 117 | }); 118 | this.modalDialogService.openDialog(this.viewContainer, { 119 | title: 'Dialog 2', 120 | childComponent: SimpleModalComponent, 121 | settings: { closeButtonClass: 'close theme-icon-close' }, 122 | placeOnTop: true, 123 | data: { text: ` 124 | Lorem ipsum is placeholder text commonly used in the graphic, print, 125 | and publishing industries for previewing layouts and visual mockups.` } 126 | }); 127 | this.modalDialogService.openDialog(this.viewContainer, { 128 | title: 'Dialog 3', 129 | childComponent: SimpleModalComponent, 130 | settings: { closeButtonClass: 'close theme-icon-close' }, 131 | placeOnTop: true, 132 | data: { text: 'Some text content' } 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /demo/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppComponent } from './app.component'; 5 | import { CustomModalComponent } from './dialogs/custom-modal.component'; 6 | import { ModalDialogModule } from 'ngx-modal-dialog'; 7 | import { DynamicModalComponent } from './dialogs/dynamic-modal.component'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | BrowserModule, 12 | ModalDialogModule.forRoot() 13 | ], 14 | declarations: [AppComponent, CustomModalComponent, DynamicModalComponent], 15 | entryComponents: [CustomModalComponent, DynamicModalComponent], 16 | bootstrap: [AppComponent] 17 | }) 18 | export class AppModule { } 19 | -------------------------------------------------------------------------------- /demo/src/app/dialogs/custom-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { IModalDialog, IModalDialogOptions } from 'ngx-modal-dialog'; 2 | import { Component, ComponentRef } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'app-custom-modal', 6 | template: ` 7 |

This component is custom.

8 |

This came from parent: {{parentInfo}}

9 | `, 10 | styles: [':host { background: lightblue; display: block; padding: 5px } '] 11 | }) 12 | export class CustomModalComponent implements IModalDialog { 13 | parentInfo: string; 14 | 15 | dialogInit(reference: ComponentRef, options: Partial>) { 16 | this.parentInfo = options.data; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /demo/src/app/dialogs/dynamic-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { IModalDialog, IModalDialogOptions } from 'ngx-modal-dialog'; 2 | import { Component, ComponentRef } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'app-dynamic-modal', 6 | template: ` 7 |

This component has dynamic buttons

8 |

You can add and change action buttons "on fly"

9 | `, 10 | styles: [':host { border: 1px solid lightgray; display: block; padding: 5px } '] 11 | }) 12 | export class DynamicModalComponent implements IModalDialog { 13 | private internalActionButtons = []; 14 | 15 | dialogInit(reference: ComponentRef, options: Partial>) { 16 | options.actionButtons = this.internalActionButtons; 17 | 18 | this.internalActionButtons.push({ 19 | text: 'Add new button', 20 | buttonClass: 'btn btn-info', 21 | onAction: () => this.addNewActionButton() 22 | }); 23 | 24 | this.internalActionButtons.push({ 25 | text: 'Close', 26 | buttonClass: 'btn btn-success', 27 | onAction: () => true 28 | }); 29 | } 30 | 31 | addNewActionButton() { 32 | this.internalActionButtons.splice(1, 0, { 33 | text: 'Rename close button', 34 | buttonClass: 'btn btn-danger', 35 | onAction: () => this.renameCloseButton() 36 | }); 37 | } 38 | 39 | renameCloseButton() { 40 | this.internalActionButtons[this.internalActionButtons.length - 1].text = 'KLOUZ'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Greentube/ngx-modal/5e210c98fff3a1bb798bbd9321a187778cd58558/demo/src/assets/.gitkeep -------------------------------------------------------------------------------- /demo/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /demo/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Greentube/ngx-modal/5e210c98fff3a1bb798bbd9321a187778cd58558/demo/src/favicon.ico -------------------------------------------------------------------------------- /demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ngx Modal Dialog 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/src/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: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /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/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /demo/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import '../node_modules/bootstrap/scss/bootstrap'; 2 | 3 | .theme-icon-close:before { 4 | content: 'X' 5 | } 6 | -------------------------------------------------------------------------------- /demo/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /demo/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "main.ts", 9 | "polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /demo/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /demo/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "downlevelIteration": true, 6 | "module": "es2020", 7 | "outDir": "./dist/out-tsc", 8 | "sourceMap": true, 9 | "declaration": false, 10 | "moduleResolution": "node", 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2017", 19 | "dom" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* 2 | This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScript’s language server to improve development experience. 3 | It is not intended to be used to perform a compilation. 4 | 5 | To learn more about this file see: https://angular.io/config/solution-tsconfig. 6 | */ 7 | { 8 | "files": [], 9 | "references": [ 10 | { 11 | "path": "./src/tsconfig.app.json" 12 | }, 13 | { 14 | "path": "./src/tsconfig.spec.json" 15 | }, 16 | { 17 | "path": "./e2e/tsconfig.e2e.json" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /demo/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-var-keyword": true, 76 | "object-literal-sort-keys": false, 77 | "one-line": [ 78 | true, 79 | "check-open-brace", 80 | "check-catch", 81 | "check-else", 82 | "check-whitespace" 83 | ], 84 | "prefer-const": true, 85 | "quotemark": [ 86 | true, 87 | "single" 88 | ], 89 | "radix": true, 90 | "semicolon": [ 91 | true, 92 | "always" 93 | ], 94 | "triple-equals": [ 95 | true, 96 | "allow-null-check" 97 | ], 98 | "typedef-whitespace": [ 99 | true, 100 | { 101 | "call-signature": "nospace", 102 | "index-signature": "nospace", 103 | "parameter": "nospace", 104 | "property-declaration": "nospace", 105 | "variable-declaration": "nospace" 106 | } 107 | ], 108 | "unified-signatures": true, 109 | "variable-name": false, 110 | "whitespace": [ 111 | true, 112 | "check-branch", 113 | "check-decl", 114 | "check-operator", 115 | "check-separator", 116 | "check-type" 117 | ], 118 | "no-output-on-prefix": true, 119 | "no-inputs-metadata-property": true, 120 | "no-outputs-metadata-property": true, 121 | "no-host-metadata-property": true, 122 | "no-input-rename": true, 123 | "no-output-rename": true, 124 | "use-lifecycle-interface": true, 125 | "use-pipe-transform-interface": true, 126 | "component-class-suffix": true, 127 | "directive-class-suffix": true 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Look in ./config for karma.conf.js 2 | module.exports = require('./config/karma.conf.js'); -------------------------------------------------------------------------------- /ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "./bundles", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-modal-dialog", 3 | "version": "4.0.0", 4 | "description": "Dynamic modal dialog for Angular", 5 | "scripts": { 6 | "test": "karma start", 7 | "test-watch": "karma start --singleRun=false --autoWatch=true", 8 | "commit": "npm run prepublish && npm test", 9 | "prepublish": "npm run build", 10 | "build": "ng build" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/Greentube/ngx-modal.git" 15 | }, 16 | "keywords": [ 17 | "Angular", 18 | "modal", 19 | "dialog" 20 | ], 21 | "author": "Miroslav Jonas", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/Greentube/ngx-modal/issues" 25 | }, 26 | "homepage": "https://github.com/Greentube/ngx-modal", 27 | "peerDependencies": { 28 | "@angular/common": ">=10.0.0", 29 | "@angular/core": ">=10.0.0", 30 | "rxjs": ">=6.5.0" 31 | }, 32 | "devDependencies": { 33 | "@angular-devkit/build-angular": "~0.1000.8", 34 | "@angular-devkit/build-ng-packagr": "~0.1000.8", 35 | "@angular/animations": "^10.0.14", 36 | "@angular/cli": "^10.0.8", 37 | "@angular/common": "^10.0.14", 38 | "@angular/compiler": "^10.0.14", 39 | "@angular/compiler-cli": "^10.0.14", 40 | "@angular/core": "^10.0.14", 41 | "@angular/forms": "^10.0.14", 42 | "@angular/platform-browser": "^10.0.14", 43 | "@angular/platform-browser-dynamic": "^10.0.14", 44 | "@angular/platform-server": "^10.0.14", 45 | "@angular/router": "^10.0.14", 46 | "@types/hammerjs": "~2.0.35", 47 | "@types/jasmine": "^3.5.14", 48 | "@types/jasminewd2": "~2.0.2", 49 | "@types/node": "^14.6.2", 50 | "awesome-typescript-loader": "^5.2.1", 51 | "clean-webpack-plugin": "^3.0.0", 52 | "codelyzer": "^6.0.0", 53 | "concurrently": "^3.3.0", 54 | "core-js": "^3.6.4", 55 | "istanbul-instrumenter-loader": "^3.0.1", 56 | "jasmine-core": "^3.6.0", 57 | "karma": "^5.2.0", 58 | "karma-chrome-launcher": "~3.1.0", 59 | "karma-coverage": "~2.0.3", 60 | "karma-jasmine": "~4.0.1", 61 | "karma-mocha-reporter": "2.2.5", 62 | "karma-remap-coverage": "0.1.5", 63 | "karma-sourcemap-loader": "0.3.8", 64 | "karma-webpack": "~4.0.2", 65 | "loader-utils": "~2.0.0", 66 | "ng-packagr": "^10.0.0", 67 | "reflect-metadata": "~0.1.12", 68 | "rxjs": "^6.6.2", 69 | "source-map-loader": "~1.1.0", 70 | "ts-helpers": "~1.1.2", 71 | "ts-node": "~8.6.2", 72 | "tslib": "^2.0.0", 73 | "tslint": "^6.1.3", 74 | "tslint-eslint-rules": "^5.4.0", 75 | "tslint-loader": "~3.6.0", 76 | "typescript": "~3.9.7", 77 | "webpack": "~4.44.1", 78 | "webpack-node-externals": "~1.7.2", 79 | "zone.js": "~0.10.3" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/modal-dialog-instance.service.ts: -------------------------------------------------------------------------------- 1 | import { ComponentRef, Injectable } from '@angular/core'; 2 | import { ModalDialogComponent } from './modal-dialog.component'; 3 | 4 | @Injectable() 5 | export class ModalDialogInstanceService { 6 | /** 7 | * Used to make sure there is exactly one instance of Modal Dialog 8 | */ 9 | private componentRefs: ComponentRef[] = []; 10 | 11 | /** 12 | * Closes existing modal dialog 13 | */ 14 | closeAnyExistingModalDialog() { 15 | while (this.componentRefs.length) { 16 | this.componentRefs[this.componentRefs.length - 1].destroy(); 17 | } 18 | } 19 | 20 | /** 21 | * Save component ref for future comparison 22 | * @param componentRef 23 | */ 24 | saveExistingModalDialog(componentRef: ComponentRef) { 25 | // remove overlay from top element 26 | this.setOverlayForTopDialog(false); 27 | // add new component 28 | this.componentRefs = [...this.componentRefs, componentRef]; 29 | componentRef.instance.showOverlay = true; 30 | 31 | componentRef.onDestroy(() => { 32 | this.componentRefs.pop(); 33 | this.setOverlayForTopDialog(true); 34 | }); 35 | } 36 | 37 | setOverlayForTopDialog(value: boolean) { 38 | if (this.componentRefs.length) { 39 | this.componentRefs[this.componentRefs.length - 1].instance.showOverlay = value; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/modal-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ComponentFactoryResolver, 4 | ComponentRef, 5 | ElementRef, 6 | HostListener, 7 | OnDestroy, 8 | OnInit, 9 | ViewChild, 10 | ViewContainerRef 11 | } from '@angular/core'; 12 | import { 13 | IModalDialog, 14 | IModalDialogButton, 15 | IModalDialogOptions, 16 | IModalDialogSettings, 17 | ModalDialogOnAction 18 | } from './modal-dialog.interface'; 19 | import { from, Observable, Subject } from 'rxjs'; 20 | 21 | /** 22 | * Modal dialog component 23 | */ 24 | @Component({ 25 | selector: 'modal-dialog', 26 | styles: [` 27 | @-moz-keyframes shake { 28 | from, to { transform: translate3d(0, 0, 0); } 29 | 10%, 30%, 50%, 70%, 90% { transform: translate3d(-2rem, 0, 0); } 30 | 20%, 40%, 60%, 80% { transform: translate3d(2rem, 0, 0); } 31 | } 32 | @-webkit-keyframes shake { 33 | from, to { transform: translate3d(0, 0, 0); } 34 | 10%, 30%, 50%, 70%, 90% { transform: translate3d(-2rem, 0, 0); } 35 | 20%, 40%, 60%, 80% { transform: translate3d(2rem, 0, 0); } 36 | } 37 | @keyframes shake { 38 | from, to { transform: translate3d(0, 0, 0); } 39 | 10%, 30%, 50%, 70%, 90% { transform: translate3d(-2rem, 0, 0); } 40 | 20%, 40%, 60%, 80% { transform: translate3d(2rem, 0, 0); } 41 | } 42 | 43 | .ngx-modal { 44 | display: flex; 45 | } 46 | .ngx-modal-shake { 47 | backface-visibility: hidden; 48 | -webkit-animation-duration: 0.5s; 49 | -moz-animation-duration: 0.5s; 50 | animation-duration: 0.5s; 51 | -webkit-animation-fill-mode: both; 52 | -moz-animation-fill-mode: both; 53 | animation-fill-mode: both; 54 | -webkit-animation-iteration-count: infinite; 55 | -moz-animation-iteration-count: infinite; 56 | animation-iteration-count: infinite; 57 | -webkit-animation-name: shake; 58 | -moz-animation-name: shake; 59 | animation-name: shake; 60 | } 61 | `], 62 | template: ` 63 |
64 |
65 |
66 |
67 |
68 |

{{title}}

69 | 73 |
74 |
75 | 76 |
77 |
78 | 81 |
82 |
83 |
84 |
85 | ` 86 | }) 87 | export class ModalDialogComponent implements IModalDialog, OnDestroy, OnInit { 88 | @ViewChild('modalDialogBody', { read: ViewContainerRef, static: true }) public dynamicComponentTarget: ViewContainerRef; 89 | @ViewChild('dialog') private dialogElement: ElementRef; 90 | public reference: ComponentRef; 91 | 92 | /** Modal dialog style settings */ 93 | public settings: IModalDialogSettings = { 94 | overlayClass: 'modal-backdrop fade', 95 | overlayAnimationTriggerClass: 'show', 96 | modalClass: 'modal ngx-modal fade', 97 | modalAnimationTriggerClass: 'show', 98 | modalDialogClass: 'modal-dialog modal-dialog-centered', 99 | contentClass: 'modal-content', 100 | headerClass: 'modal-header', 101 | headerTitleClass: 'modal-title', 102 | closeButtonClass: 'close glyphicon glyphicon-remove', 103 | closeButtonTitle: 'CLOSE', 104 | bodyClass: 'modal-body', 105 | footerClass: 'modal-footer', 106 | alertClass: 'ngx-modal-shake', 107 | alertDuration: 250, 108 | notifyWithAlert: true, 109 | buttonClass: 'btn btn-primary' 110 | }; 111 | public actionButtons: IModalDialogButton[]; 112 | public title: string; 113 | public onClose: () => Promise | Observable | boolean; 114 | 115 | public showAlert: boolean = false; 116 | public animateOverlayClass = ''; 117 | public animateModalClass = ''; 118 | 119 | public showOverlay = false; 120 | 121 | private _inProgress = false; 122 | private _alertTimeout: number; 123 | private _childInstance: any; 124 | private _closeDialog$: Subject; 125 | 126 | /** 127 | * CTOR 128 | * @param _element 129 | * @param componentFactoryResolver 130 | */ 131 | constructor(protected _element: ElementRef, 132 | private componentFactoryResolver: ComponentFactoryResolver) { 133 | } 134 | 135 | @HostListener('click', ['$event']) 136 | onClick(event: any): void { 137 | if (event.target !== this.dialogElement.nativeElement) { 138 | return; 139 | } 140 | this.close(); 141 | } 142 | 143 | /** 144 | * Initialize dialog with reference to instance and options 145 | * @param reference 146 | * @param options 147 | */ 148 | dialogInit(reference: ComponentRef, options: Partial> = {}) { 149 | this.reference = reference; 150 | 151 | // inject component 152 | if (options.childComponent) { 153 | let factory = this.componentFactoryResolver.resolveComponentFactory(options.childComponent); 154 | let componentRef = this.dynamicComponentTarget.createComponent(factory) as ComponentRef; 155 | this._childInstance = componentRef.instance as IModalDialog; 156 | 157 | this._closeDialog$ = new Subject(); 158 | this._closeDialog$.subscribe(() => { 159 | this._finalizeAndDestroy(); 160 | }); 161 | 162 | options.closeDialogSubject = this._closeDialog$; 163 | 164 | this._childInstance['dialogInit'](componentRef, options); 165 | document.activeElement != null ? 166 | (document.activeElement as HTMLElement).blur() : 167 | (document.body as HTMLElement).blur(); 168 | } 169 | // set options 170 | this._setOptions(options); 171 | } 172 | 173 | ngOnInit() { 174 | // a trick to defer css animations 175 | setTimeout(() => { 176 | this.animateOverlayClass = this.settings.overlayAnimationTriggerClass; 177 | this.animateModalClass = this.settings.modalAnimationTriggerClass; 178 | }, 0); 179 | } 180 | 181 | /** 182 | * Cleanup on destroy 183 | */ 184 | ngOnDestroy() { 185 | // run animations 186 | this.animateOverlayClass = ''; 187 | this.animateModalClass = ''; 188 | 189 | // cleanup listeners 190 | if (this._alertTimeout) { 191 | clearTimeout(this._alertTimeout); 192 | this._alertTimeout = null; 193 | } 194 | 195 | if (this._closeDialog$) { 196 | this._closeDialog$.unsubscribe(); 197 | } 198 | } 199 | 200 | /** 201 | * Run action defined on action button 202 | * @param action 203 | */ 204 | doAction(action?: ModalDialogOnAction) { 205 | // disable multi clicks 206 | if (this._inProgress) { 207 | return; 208 | } 209 | this._inProgress = true; 210 | this._closeIfSuccessful(action); 211 | } 212 | 213 | /** 214 | * Method to run on close 215 | * if action buttons are defined, it will not close 216 | */ 217 | close() { 218 | if (this._inProgress) { 219 | return; 220 | } 221 | if (this.actionButtons && this.actionButtons.length) { 222 | return; 223 | } 224 | this._inProgress = true; 225 | 226 | if (this.onClose) { 227 | this._closeIfSuccessful(this.onClose); 228 | return; 229 | } 230 | this._finalizeAndDestroy(); 231 | } 232 | 233 | /** 234 | * Pass options from dialog setup to component 235 | * @param {IModalDialogOptions} options? 236 | */ 237 | private _setOptions(options: Partial>) { 238 | 239 | if (options.onClose && options.actionButtons && options.actionButtons.length) { 240 | throw new Error(`OnClose callback and ActionButtons are not allowed to be defined on the same dialog.`); 241 | } 242 | // set references 243 | this.title = (options && options.title) || ''; 244 | this.onClose = (options && options.onClose) || null; 245 | this.actionButtons = (this._childInstance && this._childInstance['actionButtons']) || 246 | (options && options.actionButtons) || null; 247 | if (options && options.settings) { 248 | Object.assign(this.settings, options.settings); 249 | } 250 | } 251 | 252 | /** 253 | * Close if successful 254 | * @param callback 255 | */ 256 | private _closeIfSuccessful(callback: ModalDialogOnAction) { 257 | if (!callback) { 258 | return this._finalizeAndDestroy(); 259 | } 260 | 261 | let response = callback(); 262 | if (typeof response === 'boolean') { 263 | if (response) { 264 | return this._finalizeAndDestroy(); 265 | } 266 | return this._triggerAlert(); 267 | } 268 | if (this.isPromise(response)) { 269 | response = from(response); 270 | } 271 | if (this.isObservable(response)) { 272 | response.subscribe(() => { 273 | this._finalizeAndDestroy(); 274 | }, () => { 275 | this._triggerAlert(); 276 | }); 277 | } else { 278 | this._inProgress = false; 279 | } 280 | } 281 | 282 | private _finalizeAndDestroy() { 283 | this._inProgress = false; 284 | this.reference.destroy(); 285 | } 286 | 287 | private _triggerAlert() { 288 | if (this.settings.notifyWithAlert) { 289 | this.showAlert = true; 290 | this._alertTimeout = window.setTimeout(() => { 291 | this.showAlert = false; 292 | this._inProgress = false; 293 | clearTimeout(this._alertTimeout); 294 | this._alertTimeout = null; 295 | }, this.settings.alertDuration); 296 | } 297 | } 298 | 299 | private isPromise(value: any | Promise): value is Promise { 300 | return value && typeof (value).subscribe !== 'function' && typeof (value as any).then === 'function'; 301 | } 302 | 303 | private isObservable(value: any | Observable): value is Observable { 304 | return value && typeof (value).subscribe === 'function'; 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/modal-dialog.interface.ts: -------------------------------------------------------------------------------- 1 | import { ComponentRef } from '@angular/core'; 2 | import { Observable, Subject } from 'rxjs'; 3 | 4 | export interface IModalDialog { 5 | dialogInit: (reference: ComponentRef, options: Partial>) => void; 6 | } 7 | 8 | export interface IModalDialogOptions { 9 | title: string; 10 | childComponent: any; 11 | onClose: () => Promise | Observable | boolean; 12 | actionButtons: IModalDialogButton[]; 13 | data: T; 14 | placeOnTop: boolean; 15 | settings: Partial; 16 | closeDialogSubject: Subject; 17 | } 18 | 19 | export type ModalDialogOnAction = () => Promise | Observable | boolean | void; 20 | 21 | export interface IModalDialogButton { 22 | text: string; 23 | buttonClass?: string; 24 | onAction?: ModalDialogOnAction; 25 | } 26 | 27 | export interface IModalDialogSettings { 28 | overlayClass: string; 29 | overlayAnimationTriggerClass: string; 30 | modalClass: string; 31 | modalAnimationTriggerClass: string; 32 | modalDialogClass: string; 33 | contentClass: string; 34 | headerClass: string; 35 | headerTitleClass: string; 36 | closeButtonClass: string; 37 | closeButtonTitle: string; 38 | 39 | bodyClass: string; 40 | footerClass: string; 41 | alertClass: string; 42 | alertDuration: number; 43 | buttonClass: string; 44 | notifyWithAlert: boolean; 45 | } 46 | -------------------------------------------------------------------------------- /src/modal-dialog.module.ts: -------------------------------------------------------------------------------- 1 | // components and directives 2 | import { ModalDialogComponent } from './modal-dialog.component'; 3 | import { ModalDialogService } from './modal-dialog.service'; 4 | import { SimpleModalComponent } from './simple-modal.component'; 5 | import { ModalDialogInstanceService } from './modal-dialog-instance.service'; 6 | // modules 7 | import { CommonModule } from '@angular/common'; 8 | import { NgModule, ModuleWithProviders, InjectionToken, SkipSelf, Optional } from '@angular/core'; 9 | 10 | /** 11 | * Guard to make sure we have single initialization of forRoot 12 | * @type {InjectionToken} 13 | */ 14 | export const MODAL_DIALOG_FORROOT_GUARD = new InjectionToken('MODAL_DIALOG_FORROOT_GUARD'); 15 | 16 | @NgModule({ 17 | imports: [CommonModule], 18 | declarations: [ModalDialogComponent, SimpleModalComponent], 19 | entryComponents: [ModalDialogComponent, SimpleModalComponent], 20 | exports: [ModalDialogComponent, SimpleModalComponent], 21 | providers: [ModalDialogService, ModalDialogInstanceService] 22 | }) 23 | export class ModalDialogModule { 24 | 25 | public static forRoot(): ModuleWithProviders { 26 | return { 27 | ngModule: ModalDialogModule, 28 | providers: [ 29 | { 30 | provide: MODAL_DIALOG_FORROOT_GUARD, 31 | useFactory: provideForRootGuard, 32 | deps: [ModalDialogModule, new Optional(), new SkipSelf()] 33 | }, 34 | ModalDialogInstanceService 35 | ] 36 | }; 37 | } 38 | } 39 | 40 | /** 41 | * @param dialogModule 42 | * @returns {string} 43 | */ 44 | export function provideForRootGuard(dialogModule: ModalDialogModule): string { 45 | if (dialogModule) { 46 | throw new Error( 47 | `ModalDialogModule.forRoot() called twice.`); 48 | } 49 | return 'guarded'; 50 | } 51 | -------------------------------------------------------------------------------- /src/modal-dialog.service.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFactoryResolver, ViewContainerRef, Inject, Injectable } from '@angular/core'; 2 | import { ModalDialogComponent } from './modal-dialog.component'; 3 | import { IModalDialogOptions } from './modal-dialog.interface'; 4 | import { ModalDialogInstanceService } from './modal-dialog-instance.service'; 5 | 6 | @Injectable() 7 | export class ModalDialogService { 8 | /** 9 | * CTOR 10 | * @param componentFactoryResolver 11 | * @param modalDialogInstanceService 12 | */ 13 | constructor(@Inject(ComponentFactoryResolver) private componentFactoryResolver: ComponentFactoryResolver, 14 | @Inject(ModalDialogInstanceService) private modalDialogInstanceService: ModalDialogInstanceService) { 15 | } 16 | 17 | /** 18 | * Open dialog in given target element with given options 19 | * @param {ViewContainerRef} target 20 | * @param {IModalDialogOptions} options? 21 | */ 22 | openDialog(target: ViewContainerRef, options: Partial> = {}) { 23 | if (!options.placeOnTop) { 24 | this.modalDialogInstanceService.closeAnyExistingModalDialog(); 25 | } 26 | 27 | const factory = this.componentFactoryResolver.resolveComponentFactory(ModalDialogComponent); 28 | const componentRef = target.createComponent(factory); 29 | componentRef.instance.dialogInit(componentRef, options); 30 | 31 | this.modalDialogInstanceService.saveExistingModalDialog(componentRef); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-modal-dialog 3 | */ 4 | 5 | export * from './modal-dialog.module'; 6 | export * from './modal-dialog.service'; 7 | export * from './modal-dialog-instance.service'; 8 | export * from './modal-dialog.component'; 9 | export * from './modal-dialog.interface'; 10 | export * from './simple-modal.component'; 11 | -------------------------------------------------------------------------------- /src/simple-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentRef } from '@angular/core'; 2 | import { IModalDialog, IModalDialogOptions } from './modal-dialog.interface'; 3 | 4 | export interface ISimpleModalDataOptions { 5 | text: string; 6 | } 7 | 8 | @Component({ 9 | selector: 'simple-modal-dialog', 10 | template: ``, 11 | styles: [':host { display: block; }'], 12 | host: { 13 | '[innerHTML]': 'text' 14 | } 15 | }) 16 | export class SimpleModalComponent implements IModalDialog { 17 | text: string; 18 | 19 | /** 20 | * Initialize dialog with simple HTML content 21 | * @param {ComponentRef} reference 22 | * @param {Partial} options 23 | */ 24 | dialogInit(reference: ComponentRef, options: Partial>) { 25 | if (!options.data) { 26 | throw new Error(`Data information for simple modal dialog is missing`); 27 | } 28 | this.text = options.data.text; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/modal-dialog-instance.service.spec.ts: -------------------------------------------------------------------------------- 1 | // import { ComponentRef } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | 4 | import { ModalDialogComponent } from '../src/modal-dialog.component'; 5 | import { ModalDialogInstanceService } from '../src/modal-dialog-instance.service'; 6 | 7 | // let compRef: ComponentRef = { 8 | // instance: null, 9 | // location: null, 10 | // injector: null, 11 | // hostView: null, 12 | // changeDetectorRef: null, 13 | // componentType: null, 14 | // onDestroy: null, 15 | // destroy() { 16 | // } 17 | // }; 18 | 19 | describe('ModalDialogInstance.Service: ', () => { 20 | 21 | beforeEach(() => { 22 | TestBed.configureTestingModule({ 23 | providers: [ 24 | ModalDialogInstanceService 25 | ], 26 | declarations: [ModalDialogComponent] 27 | }); 28 | }); 29 | 30 | it('should create ModalDialogInstanceService', () => { 31 | let service = new ModalDialogInstanceService(); 32 | expect(service.closeAnyExistingModalDialog).toBeDefined(); 33 | expect(service.saveExistingModalDialog).toBeDefined(); 34 | }); 35 | 36 | // it('should save and close dialog', () => { 37 | // let service = new ModalDialogInstanceService(); 38 | // 39 | // spyOn(compRef, 'destroy'); 40 | // service.saveExistingModalDialog(> compRef); 41 | // service.closeAnyExistingModalDialog(); 42 | // expect(compRef.destroy).toHaveBeenCalled(); 43 | // }); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/modal-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentRef, Component } from '@angular/core'; 2 | import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; 5 | import { ModalDialogComponent } from '../src/modal-dialog.component'; 6 | import { IModalDialog, IModalDialogOptions } from '../src/modal-dialog.interface'; 7 | import { CommonModule } from '@angular/common'; 8 | import { Subject, of } from 'rxjs'; 9 | 10 | let fixture: ComponentFixture; 11 | 12 | @Component({ 13 | selector: 'dummy', 14 | template: `{{props}}` 15 | }) 16 | class DummyComponent implements IModalDialog { 17 | props: any; 18 | closingSubject$: Subject; 19 | 20 | dialogInit(reference: ComponentRef, options: Partial> = {}) { 21 | this.props = options.data; 22 | this.closingSubject$ = options.closeDialogSubject; 23 | } 24 | 25 | closeMe() { 26 | if (this.closingSubject$) { 27 | this.closingSubject$.next(); 28 | } 29 | } 30 | } 31 | 32 | describe('ModalDialog.Component:', () => { 33 | let component: ModalDialogComponent; 34 | let sampleText: string; 35 | let onCloseWrapper: any; 36 | let data: any; 37 | let actionButtons: any[]; 38 | 39 | beforeEach(() => { 40 | jasmine.clock().uninstall(); 41 | jasmine.clock().install(); 42 | 43 | let module = TestBed.configureTestingModule({ 44 | imports: [CommonModule], 45 | declarations: [ModalDialogComponent, DummyComponent] 46 | }); 47 | module.overrideModule(BrowserDynamicTestingModule, { 48 | set: { entryComponents: [DummyComponent] } 49 | }); 50 | 51 | fixture = module.createComponent(ModalDialogComponent); 52 | component = fixture.componentInstance; 53 | }); 54 | 55 | beforeEach(() => { 56 | sampleText = 'sample text'; 57 | data = { some: 'data' }; 58 | onCloseWrapper = { 59 | onClose: () => new Promise((resolve: any) => { 60 | resolve(); 61 | }) 62 | }; 63 | actionButtons = [ 64 | { text: 'abc', onAction: () => { 65 | return new Promise((resolve: any) => { 66 | resolve(); 67 | }); 68 | } }, 69 | { text: 'def', onAction: () => true }, 70 | { text: 'hgi', onAction: () => of(true) } 71 | ]; 72 | }); 73 | 74 | it('should initialize component and define dialogInit method', () => { 75 | expect(component.dialogInit).toBeDefined('should define dialogInit method'); 76 | }); 77 | 78 | it('should set methods from options and initialize component', () => { 79 | sampleText = 'sample text'; 80 | let onClose = () => new Promise((resolve: any) => { 81 | resolve(); 82 | }); 83 | data = { some: 'data' }; 84 | 85 | component.dialogInit(fixture.componentRef, { 86 | title: sampleText, 87 | onClose: onClose, 88 | data: data 89 | }); 90 | 91 | // TODO: Fix those 92 | // expect(component.title).toEqual(sampleText, 'title should equal sample text'); 93 | // expect(component.onClose).toEqual(onClose, 'onClose should eqaul defined function'); 94 | // expect(component.data).toEqual(data, 'data should equal defined value'); 95 | }); 96 | 97 | it('should set default values if no options passed', () => { 98 | component.dialogInit(fixture.componentRef); 99 | 100 | // TODO: Fix those 101 | // expect(component.title).toEqual('', 'default title should be ""'); 102 | // expect(component.onClose).toEqual(null, 'onClose should be null'); 103 | // expect(component.prompt).toEqual(null, 'prompt should be null'); 104 | // expect(component.data).toEqual(null, 'data should be null'); 105 | }); 106 | 107 | it('should throw exception if both onClose and prompt are set', () => { 108 | function testFunction() { 109 | component.dialogInit(fixture.componentRef, { 110 | title: sampleText, 111 | onClose: onCloseWrapper.onClose, 112 | actionButtons: actionButtons, 113 | data: data 114 | }); 115 | } 116 | 117 | // act + assert 118 | expect(testFunction).toThrowError(/OnClose callback and ActionButtons/); 119 | 120 | component.close(); 121 | }); 122 | 123 | it('should call onAction callback on button click and remove component reference after', 124 | fakeAsync(() => { 125 | component.dialogInit(fixture.componentRef, { 126 | title: sampleText, 127 | actionButtons: actionButtons, 128 | data: data 129 | }); 130 | // pre-check 131 | spyOn(actionButtons[0], 'onAction').and.callThrough(); 132 | spyOn(fixture.componentRef, 'destroy').and.callThrough(); 133 | // act 134 | component.doAction(actionButtons[0].onAction); 135 | // assert 136 | expect(actionButtons[0].onAction).toHaveBeenCalled(); 137 | tick(); 138 | expect(fixture.componentRef.destroy).toHaveBeenCalled(); 139 | }) 140 | ); 141 | 142 | it('should not remove component if onAction fails and show alert', fakeAsync(() => { 143 | component.dialogInit(fixture.componentRef, { 144 | title: sampleText, 145 | actionButtons: actionButtons, 146 | data: data 147 | }); 148 | actionButtons[0].onAction = () => new Promise((resolve: any, reject: any) => { 149 | reject(); 150 | }); 151 | fixture.detectChanges(); 152 | let modalDialog = fixture.nativeElement.querySelector('.modal-content'); 153 | // pre-check 154 | spyOn(fixture.componentRef, 'destroy').and.callThrough(); 155 | // act 156 | component.doAction(actionButtons[0].onAction); 157 | // assert 158 | tick(); 159 | fixture.detectChanges(); 160 | expect(modalDialog.className).toMatch(/shake/); 161 | jasmine.clock().tick(300); 162 | fixture.detectChanges(); 163 | expect(modalDialog.className).not.toMatch(/shake/); 164 | 165 | expect(fixture.componentRef.destroy).not.toHaveBeenCalled(); 166 | })); 167 | 168 | it('should call onAction callback only once', 169 | fakeAsync(() => { 170 | component.dialogInit(fixture.componentRef, { 171 | title: sampleText, 172 | actionButtons: actionButtons, 173 | data: data 174 | }); 175 | // pre-check 176 | spyOn(actionButtons[0], 'onAction').and.callThrough(); 177 | spyOn(fixture.componentRef, 'destroy').and.callThrough(); 178 | // act 179 | component.doAction(actionButtons[0].onAction); 180 | component.doAction(actionButtons[0].onAction); 181 | // assert 182 | expect(actionButtons[0].onAction.calls.count()).toEqual(1); 183 | tick(); 184 | }) 185 | ); 186 | 187 | it('should remove component reference on accept button even if no prompt defined', 188 | fakeAsync(() => { 189 | component.dialogInit(fixture.componentRef, { 190 | title: sampleText, 191 | data: data 192 | }); 193 | // pre-check 194 | spyOn(fixture.componentRef, 'destroy').and.callThrough(); 195 | // act 196 | component.doAction(actionButtons[0].onAction); 197 | // assert 198 | tick(); 199 | expect(fixture.componentRef.destroy).toHaveBeenCalled(); 200 | }) 201 | ); 202 | 203 | it('should remove component reference on doAction even if no callback defined', fakeAsync(() => { 204 | component.dialogInit(fixture.componentRef, { 205 | title: sampleText, 206 | data: data 207 | }); 208 | // pre-check 209 | spyOn(fixture.componentRef, 'destroy').and.callThrough(); 210 | // act 211 | component.doAction(); 212 | // assert 213 | tick(); 214 | expect(fixture.componentRef.destroy).toHaveBeenCalled(); 215 | })); 216 | 217 | it('should remove reference on close button', 218 | fakeAsync(() => { 219 | component.dialogInit(fixture.componentRef, { 220 | title: sampleText, 221 | data: data 222 | }); 223 | // pre-check 224 | spyOn(fixture.componentRef, 'destroy').and.callThrough(); 225 | // act 226 | component.close(); 227 | // assert 228 | tick(); 229 | expect(fixture.componentRef.destroy).toHaveBeenCalled(); 230 | }) 231 | ); 232 | 233 | it('should create the component', () => { 234 | expect(fixture.componentInstance instanceof ModalDialogComponent) 235 | .toBe(true, 'should create ModalDialogComponent'); 236 | }); 237 | 238 | it('should inject new component', fakeAsync(() => { 239 | let testString = 'some data'; 240 | 241 | component.dialogInit(fixture.componentRef, { 242 | childComponent: DummyComponent, 243 | data: testString 244 | }); 245 | 246 | fixture.detectChanges(); 247 | // flush async calls 248 | tick(); 249 | fixture.detectChanges(); 250 | 251 | let innerComponent = fixture.debugElement.query(By.css('.modal-body')).nativeElement; 252 | 253 | expect(innerComponent).toBeDefined('modal dialog body should be defined'); 254 | expect(innerComponent.innerHTML).toContain(testString); 255 | })); 256 | 257 | it('should close dialog from child component', fakeAsync(() => { 258 | let testString = 'some data'; 259 | 260 | component.dialogInit(fixture.componentRef, { 261 | childComponent: DummyComponent, 262 | data: testString 263 | }); 264 | 265 | fixture.detectChanges(); 266 | 267 | const dummyDebugElem = fixture.debugElement.query(By.css('dummy')); 268 | const dummyComponent = dummyDebugElem.injector.get(DummyComponent) as DummyComponent; 269 | 270 | spyOn(fixture.componentRef, 'destroy').and.callThrough(); 271 | spyOn(dummyComponent, 'closeMe').and.callThrough(); 272 | 273 | // act 274 | dummyComponent.closeMe(); 275 | tick(); 276 | 277 | // assert 278 | expect(dummyComponent.closeMe).toHaveBeenCalled(); 279 | expect(fixture.componentRef.destroy).toHaveBeenCalled(); 280 | })); 281 | 282 | it('should unsubscribe from subject when closing dialog from child component', fakeAsync(() => { 283 | let testString = 'some data'; 284 | 285 | component.dialogInit(fixture.componentRef, { 286 | childComponent: DummyComponent, 287 | data: testString 288 | }); 289 | 290 | fixture.detectChanges(); 291 | 292 | const dummyDebugElem = fixture.debugElement.query(By.css('dummy')); 293 | const dummyComponent = dummyDebugElem.injector.get(DummyComponent) as DummyComponent; 294 | 295 | spyOn(dummyComponent.closingSubject$, 'unsubscribe').and.callThrough(); 296 | spyOn(dummyComponent, 'closeMe').and.callThrough(); 297 | 298 | // act 299 | dummyComponent.closeMe(); 300 | tick(); 301 | 302 | // assert 303 | expect(dummyComponent.closeMe).toHaveBeenCalled(); 304 | expect(dummyComponent.closingSubject$.unsubscribe).toHaveBeenCalled(); 305 | })); 306 | }); 307 | -------------------------------------------------------------------------------- /tests/modal-dialog.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFactoryResolver, ComponentRef, ViewContainerRef } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | 4 | import { IModalDialogOptions, IModalDialog } from '../src/modal-dialog.interface'; 5 | import { ModalDialogComponent } from '../src/modal-dialog.component'; 6 | import { ModalDialogService } from '../src/modal-dialog.service'; 7 | import { ModalDialogInstanceService } from '../src/modal-dialog-instance.service'; 8 | 9 | let compRef = { 10 | instance: { 11 | dialogInit: (reference: ComponentRef, options: Partial> = {}) => { /**/ 12 | } 13 | }, 14 | destroy() { 15 | } 16 | }; 17 | 18 | class MockedViewContainerRef { 19 | createComponent(input: string) { 20 | return compRef; 21 | } 22 | } 23 | 24 | class MockedComponentFactoryResolver { 25 | resolveComponentFactory(type: any): string { 26 | return typeof type; 27 | } 28 | } 29 | 30 | describe('ModalDialog.Service: ', () => { 31 | beforeEach(() => { 32 | 33 | TestBed.configureTestingModule({ 34 | providers: [ 35 | ModalDialogService, 36 | { provide: ViewContainerRef, useClass: MockedViewContainerRef }, 37 | { provide: ComponentFactoryResolver, useClass: MockedComponentFactoryResolver }, 38 | ModalDialogInstanceService 39 | ], 40 | declarations: [ModalDialogComponent] 41 | }); 42 | }); 43 | 44 | //let component; 45 | let componentFactoryResolver: any; 46 | // let viewRef: any; 47 | 48 | beforeEach(() => { 49 | componentFactoryResolver = new MockedComponentFactoryResolver(); 50 | // viewRef = new MockedViewContainerRef(); 51 | }); 52 | 53 | it('should create DataCenterService', () => { 54 | let instanceService = new ModalDialogInstanceService(); 55 | let service = new ModalDialogService(componentFactoryResolver, instanceService); 56 | expect(service.openDialog).toBeDefined(); 57 | }); 58 | 59 | // it('should call DynamicComponentLoader.loadNextToLocation on openDialog', () => { 60 | // 61 | // //arrange 62 | // let instanceService = new ModalDialogInstanceService(); 63 | // let service = new ModalDialogService(componentFactoryResolver, instanceService); 64 | // let options = { title: 'ABC' }; 65 | // 66 | // //act 67 | // spyOn(componentFactoryResolver, 'resolveComponentFactory').and.callThrough(); 68 | // spyOn(compRef.instance, 'dialogInit').and.stub(); 69 | // spyOn(compRef, 'destroy'); 70 | // 71 | // service.openDialog(viewRef, options); 72 | // 73 | // //assert 74 | // expect(componentFactoryResolver.resolveComponentFactory).toHaveBeenCalledWith(ModalDialogComponent); 75 | // expect(compRef.instance.dialogInit).toHaveBeenCalledWith(compRef, options); 76 | // 77 | // //Assert that the destroy has been called in the compRef 78 | // service.openDialog(viewRef, options); 79 | // expect(compRef.destroy).toHaveBeenCalled(); 80 | // }); 81 | }); 82 | -------------------------------------------------------------------------------- /tests/simple-modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, ComponentFixture } from '@angular/core/testing'; 2 | import { SimpleModalComponent } from '../src/simple-modal.component'; 3 | 4 | let fixture: ComponentFixture; 5 | 6 | describe('SimpleModal.Component: ', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | declarations: [SimpleModalComponent] 10 | }); 11 | fixture = TestBed.createComponent(SimpleModalComponent); 12 | }); 13 | 14 | it('should create component', () => { 15 | expect(fixture).toBeDefined(); 16 | }); 17 | 18 | it('should create component and pass text', () => { 19 | let testText = 'SOME TEXT'; 20 | let instance = fixture.componentInstance; 21 | instance.dialogInit(fixture.componentRef, { 22 | data: { text: testText } 23 | }); 24 | fixture.detectChanges(); 25 | let self = fixture.debugElement.nativeElement; 26 | 27 | expect(self.innerHTML).toEqual(testText); 28 | }); 29 | it('should throw exception if no data in options', () => { 30 | let instance = fixture.componentInstance; 31 | 32 | expect(() => { 33 | instance.dialogInit(fixture.componentRef, {}); 34 | }).toThrowError(/Data information for simple modal dialog is missing/); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "noImplicitAny": true, 5 | "outDir": "./bundles/out-tsc", 6 | "module": "es2020", 7 | "target": "es5", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "declaration": true, 11 | "moduleResolution": "node", 12 | "noUnusedLocals": true, 13 | "stripInternal": true, 14 | "allowSyntheticDefaultImports": true, 15 | "noUnusedParameters": false, 16 | "types": [ 17 | "hammerjs", 18 | "jasmine", 19 | "node" 20 | ], 21 | "lib": ["es7", "es2015", "dom"] 22 | }, 23 | "files": [ 24 | "./src/modal-dialog.module.ts", 25 | "./src/modal-dialog.component.ts", 26 | "./src/modal-dialog.interface.ts", 27 | "./src/modal-dialog.service.ts", 28 | "./src/modal-dialog-instance.service.ts", 29 | "./src/simple-modal.component.ts", 30 | "./tests/modal-dialog.component.spec.ts", 31 | "./tests/modal-dialog.service.spec.ts", 32 | "./tests/modal-dialog-instance.service.spec.ts", 33 | "./tests/simple-modal.component.spec.ts" 34 | ], 35 | "angularCompilerOptions": { 36 | "fullTemplateTypeCheck": true, 37 | "strictInjectionParameters": true, 38 | "enableIvy": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /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 | "skipTemplateCodegen": true, 16 | "strictMetadataEmit": true, 17 | "enableResourceInlining": true, 18 | "enableIvy": false 19 | }, 20 | "exclude": [ 21 | "test.ts", 22 | "**/*.spec.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer", 4 | "node_modules/tslint-eslint-rules" 5 | ], 6 | "extends": [ 7 | "tslint-eslint-rules", 8 | "codelyzer" 9 | ], 10 | "rules": { 11 | "class-name": true, 12 | "curly": true, 13 | "deprecation": { 14 | "severity": "warning" 15 | }, 16 | "forin": true, 17 | "indent": [ true, "spaces", 2 ], 18 | "label-position": true, 19 | "member-access": false, 20 | "no-arg": true, 21 | "no-bitwise": true, 22 | "no-console": [ true, "log" ], 23 | "no-construct": true, 24 | "no-debugger": true, 25 | "no-duplicate-variable": true, 26 | "no-empty": false, 27 | "no-eval": true, 28 | "no-inferrable-types": false, 29 | "no-shadowed-variable": true, 30 | "no-string-literal": false, 31 | "no-unused-expression": true, 32 | "object-literal-sort-keys": false, 33 | "one-line": [ 34 | true, 35 | "check-open-brace", 36 | "check-catch", 37 | "check-else", 38 | "check-whitespace" 39 | ], 40 | "radix": true, 41 | "semicolon": [true, "always"], 42 | "triple-equals": [ 43 | true, 44 | "allow-null-check" 45 | ], 46 | "typedef-whitespace": [ 47 | true, 48 | { 49 | "call-signature": "nospace", 50 | "index-signature": "nospace", 51 | "parameter": "nospace", 52 | "property-declaration": "nospace", 53 | "variable-declaration": "nospace" 54 | } 55 | ], 56 | "variable-name": false, 57 | "use-input-property-decorator": true, 58 | "use-output-property-decorator": true, 59 | "use-host-property-decorator": false, 60 | "use-life-cycle-interface": true, 61 | "use-pipe-transform-interface": true, 62 | "no-consecutive-blank-lines": [true, 1], 63 | "whitespace": [ 64 | true, 65 | "check-decl", 66 | "check-operator", 67 | "check-module", 68 | "check-separator", 69 | "check-type" 70 | ], 71 | "trailing-comma": true, 72 | "quotemark": [true, "single", "avoid-escape"], 73 | 74 | "object-curly-spacing": [true, "always"] 75 | } 76 | } 77 | --------------------------------------------------------------------------------