├── .all-contributorsrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .github
├── actions
│ └── node-setup
│ │ └── action.yml
├── issue_template.md
├── pull_request_template.md
└── workflows
│ └── ci.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .prettierignore
├── .vscode
└── extensions.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── apps
└── playground
│ ├── .eslintrc.json
│ ├── karma.conf.js
│ ├── project.json
│ ├── src
│ ├── app
│ │ ├── app.component.html
│ │ ├── app.component.scss
│ │ ├── app.component.ts
│ │ ├── reset-location-dialog.component.ts
│ │ └── test-dialog.component.ts
│ ├── assets
│ │ └── .gitkeep
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── favicon.svg
│ ├── index.html
│ ├── main.ts
│ ├── polyfills.ts
│ ├── styles.scss
│ └── test.ts
│ ├── tsconfig.app.json
│ └── tsconfig.spec.json
├── changelog.config.js
├── commitlint.config.js
├── karma.conf.js
├── libs
└── dialog
│ ├── .eslintrc.json
│ ├── CHANGELOG.md
│ ├── karma.conf.js
│ ├── ng-package.json
│ ├── package.json
│ ├── project.json
│ ├── src
│ ├── lib
│ │ ├── close-all-dialogs.directive.ts
│ │ ├── dialog-close.directive.ts
│ │ ├── dialog-ref.ts
│ │ ├── dialog.component.scss
│ │ ├── dialog.component.ts
│ │ ├── dialog.service.ts
│ │ ├── dialog.utils.ts
│ │ ├── draggable.directive.ts
│ │ ├── providers.ts
│ │ ├── specs
│ │ │ ├── dialog-close.directive.spec.ts
│ │ │ ├── dialog.component.spec.ts
│ │ │ ├── dialog.service.spec.ts
│ │ │ ├── dialog.spec.ts
│ │ │ └── types.spec.ts
│ │ └── types.ts
│ ├── public-api.ts
│ └── test.ts
│ ├── tsconfig.lib.json
│ ├── tsconfig.lib.prod.json
│ └── tsconfig.spec.json
├── logo.svg
├── migrations.json
├── nx.json
├── package-lock.json
├── package.json
├── prettier.config.js
├── scripts
├── post-build.js
└── pre-commit.js
├── tools
├── generators
│ └── .gitkeep
└── tsconfig.tools.json
└── tsconfig.base.json
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "dialog",
3 | "projectOwner": "@ngneat",
4 | "repoType": "github",
5 | "repoHost": "https://github.com",
6 | "files": [
7 | "README.md"
8 | ],
9 | "imageSize": 100,
10 | "commit": true,
11 | "commitConvention": "angular",
12 | "contributors": [
13 | {
14 | "login": "tonivj5",
15 | "name": "Toni Villena",
16 | "avatar_url": "https://avatars2.githubusercontent.com/u/7110786?v=4",
17 | "profile": "https://github.com/tonivj5",
18 | "contributions": [
19 | "code",
20 | "infra",
21 | "test"
22 | ]
23 | },
24 | {
25 | "login": "NetanelBasal",
26 | "name": "Netanel Basal",
27 | "avatar_url": "https://avatars1.githubusercontent.com/u/6745730?v=4",
28 | "profile": "https://www.netbasal.com/",
29 | "contributions": [
30 | "doc",
31 | "ideas",
32 | "code"
33 | ]
34 | },
35 | {
36 | "login": "theblushingcrow",
37 | "name": "Inbal Sinai",
38 | "avatar_url": "https://avatars3.githubusercontent.com/u/638818?v=4",
39 | "profile": "https://github.com/theblushingcrow",
40 | "contributions": [
41 | "doc"
42 | ]
43 | },
44 | {
45 | "login": "shaharkazaz",
46 | "name": "Shahar Kazaz",
47 | "avatar_url": "https://avatars2.githubusercontent.com/u/17194830?v=4",
48 | "profile": "https://github.com/shaharkazaz",
49 | "contributions": [
50 | "code",
51 | "doc"
52 | ]
53 | },
54 | {
55 | "login": "beeman",
56 | "name": "beeman",
57 | "avatar_url": "https://avatars3.githubusercontent.com/u/36491?v=4",
58 | "profile": "https://github.com/beeman",
59 | "contributions": [
60 | "code"
61 | ]
62 | },
63 | {
64 | "login": "rhutchison",
65 | "name": "Ryan Hutchison",
66 | "avatar_url": "https://avatars.githubusercontent.com/u/1460261?v=4",
67 | "profile": "https://github.com/rhutchison",
68 | "contributions": [
69 | "code",
70 | "ideas"
71 | ]
72 | },
73 | {
74 | "login": "Langstra",
75 | "name": "Wybren Kortstra",
76 | "avatar_url": "https://avatars.githubusercontent.com/u/1962982?v=4",
77 | "profile": "https://riskchallenger.nl/",
78 | "contributions": [
79 | "code"
80 | ]
81 | }
82 | ],
83 | "contributorsPerLine": 7
84 | }
85 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "ignorePatterns": ["**/*"],
4 | "plugins": ["@nx"],
5 | "settings": {
6 | "import/resolver": {
7 | "typescript": {
8 | "project": ["libs/*/tsconfig.json", "tsconfig.base.json"]
9 | }
10 | }
11 | },
12 | "overrides": [
13 | {
14 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
15 | "rules": {
16 | "@nx/enforce-module-boundaries": [
17 | "error",
18 | {
19 | "enforceBuildableLibDependency": true,
20 | "allow": [],
21 | "depConstraints": [
22 | {
23 | "sourceTag": "*",
24 | "onlyDependOnLibsWithTags": ["*"]
25 | }
26 | ]
27 | }
28 | ]
29 | }
30 | },
31 | {
32 | "files": ["*.ts", "*.tsx"],
33 | "extends": ["plugin:@nx/typescript", "plugin:import/recommended", "plugin:import/typescript"],
34 | "rules": {
35 | "import/order": [
36 | "error",
37 | {
38 | "groups": ["builtin", "external", "internal", "parent", "sibling"],
39 | "newlines-between": "always"
40 | }
41 | ]
42 | }
43 | },
44 | {
45 | "files": ["*.js", "*.jsx"],
46 | "extends": ["plugin:@nx/javascript"],
47 | "rules": {}
48 | },
49 | {
50 | "files": ["**/*.spec.ts", "**/mocks.ts", "**/test-setup.ts"],
51 | "rules": {
52 | "@typescript-eslint/no-explicit-any": "off",
53 | "@angular-eslint/component-class-suffix": "off",
54 | "@typescript-eslint/no-empty-function": "off",
55 | "@typescript-eslint/no-non-null-assertion": "off"
56 | }
57 | },
58 | {
59 | "files": ["**/types.ts", "**/helpers.ts"],
60 | "rules": {
61 | "@typescript-eslint/no-explicit-any": "off"
62 | }
63 | }
64 | ]
65 | }
66 |
--------------------------------------------------------------------------------
/.github/actions/node-setup/action.yml:
--------------------------------------------------------------------------------
1 | name: 'Setup Node and install dependencies'
2 | description: 'Setup Node and install dependencies'
3 |
4 | runs:
5 | using: 'composite'
6 | steps:
7 | - uses: actions/setup-node@v3
8 | with:
9 | node-version: 18.17.0
10 | cache: 'npm'
11 |
12 | - name: Install dependencies
13 | run: npm ci
14 | shell: bash
15 |
--------------------------------------------------------------------------------
/.github/issue_template.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | ## I'm submitting a...
8 |
9 |
10 |
11 | [ ] Regression (a behavior that used to work and stopped working in a new release)
12 | [ ] Bug report
13 | [ ] Performance issue
14 | [ ] Feature request
15 | [ ] Documentation issue or request
16 | [ ] Support request
17 | [ ] Other... Please describe:
18 |
19 |
20 | ## Current behavior
21 |
22 |
23 |
24 | ## Expected behavior
25 |
26 |
27 |
28 | ## Minimal reproduction of the problem with instructions
29 |
30 | ## What is the motivation / use case for changing the behavior?
31 |
32 |
33 |
34 | ## Environment
35 |
36 |
37 | Angular version: X.Y.Z
38 |
39 |
40 | Browser:
41 | - [ ] Chrome (desktop) version XX
42 | - [ ] Chrome (Android) version XX
43 | - [ ] Chrome (iOS) version XX
44 | - [ ] Firefox version XX
45 | - [ ] Safari (desktop) version XX
46 | - [ ] Safari (iOS) version XX
47 | - [ ] IE version XX
48 | - [ ] Edge version XX
49 |
50 | For Tooling issues:
51 | - Node version: XX
52 | - Platform:
53 |
54 | Others:
55 |
56 |
57 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## PR Checklist
2 |
3 | Please check if your PR fulfills the following requirements:
4 |
5 | - [ ] The commit message follows our guidelines: CONTRIBUTING.md#commit
6 | - [ ] Tests for the changes have been added (for bug fixes / features)
7 | - [ ] Docs have been added / updated (for bug fixes / features)
8 |
9 | ## PR Type
10 |
11 | What kind of change does this PR introduce?
12 |
13 |
14 |
15 | ```
16 | [ ] Bugfix
17 | [ ] Feature
18 | [ ] Code style update (formatting, local variables)
19 | [ ] Refactoring (no functional changes, no api changes)
20 | [ ] Build related changes
21 | [ ] CI related changes
22 | [ ] Documentation content changes
23 | [ ] Other... Please describe:
24 | ```
25 |
26 | ## What is the current behavior?
27 |
28 |
29 |
30 | Issue Number: N/A
31 |
32 | ## What is the new behavior?
33 |
34 | ## Does this PR introduce a breaking change?
35 |
36 | ```
37 | [ ] Yes
38 | [ ] No
39 | ```
40 |
41 |
42 |
43 | ## Other information
44 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 |
8 | jobs:
9 | build-lib:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v3
14 |
15 | - name: Setup node
16 | uses: ./.github/actions/node-setup
17 |
18 | - name: Build
19 | run: npm run build:lib
20 |
21 | build-playground:
22 | runs-on: ubuntu-latest
23 |
24 | steps:
25 | - uses: actions/checkout@v3
26 |
27 | - name: Setup node
28 | uses: ./.github/actions/node-setup
29 |
30 | - name: Build playground
31 | run: npm run build
32 |
33 | test:
34 | runs-on: ubuntu-latest
35 |
36 | steps:
37 | - uses: actions/checkout@v3
38 |
39 | - name: Setup node
40 | uses: ./.github/actions/node-setup
41 |
42 | - name: Run tests
43 | run: npm run ci:test
44 |
45 | lint:
46 | runs-on: ubuntu-latest
47 |
48 | steps:
49 | - uses: actions/checkout@v3
50 |
51 | - name: Setup node
52 | uses: ./.github/actions/node-setup
53 |
54 | - name: Run tests
55 | run: npm run ci:lint
56 |
--------------------------------------------------------------------------------
/.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 |
41 | .angular
42 | .nx
43 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npm run hooks:pre-commit && npx lint-staged
5 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Add files here to ignore them from prettier formatting
2 | /dist
3 | /coverage
4 | /.nx/cache
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["nrwl.angular-console", "angular.ng-template", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/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 netanel7799@gmail.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 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Dialog
2 |
3 | 🙏 We would ❤️ for you to contribute to Dialog and help make it even better than it is today!
4 |
5 | # Developing
6 |
7 | Start by installing all dependencies:
8 |
9 | ```bash
10 | npm i
11 | ```
12 |
13 | Run the tests:
14 |
15 | ```bash
16 | npm run test:lib
17 | ```
18 |
19 | Run the playground app:
20 |
21 | ```bash
22 | npm start
23 | ```
24 |
25 | ## Building
26 |
27 | ```bash
28 | npm run build
29 | ```
30 |
31 | ## Coding Rules
32 |
33 | To ensure consistency throughout the source code, keep these rules in mind as you are working:
34 |
35 | - All features or bug fixes **must be tested** by one or more specs (unit-tests).
36 | - All public API methods **must be documented**.
37 |
38 | ## Commit Message Guidelines
39 |
40 | We have very precise rules over how our git commit messages can be formatted. This leads to **more
41 | readable messages** that are easy to follow when looking through the **project history**. But also,
42 | we use the git commit messages to **generate the Dialog changelog**.
43 |
44 | ### Commit Message Format
45 |
46 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special
47 | format that includes a **type**, a **scope** and a **subject**:
48 |
49 | ```
50 | ():
51 |
52 |
53 |
54 |
55 | ```
56 |
57 | The **header** is mandatory and the **scope** of the header is optional.
58 |
59 | Any line of the commit message cannot be longer 100 characters! This allows the message to be easier
60 | to read on GitHub as well as in various git tools.
61 |
62 | The footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
63 |
64 | Samples: (even more [samples](https://github.com/angular/angular/commits/master))
65 |
66 | ```
67 | docs(changelog): update changelog to beta.5
68 | ```
69 |
70 | ```
71 | fix(release): need to depend on latest rxjs and zone.js
72 |
73 | The version in our package.json gets copied to the one we publish, and users need the latest of these.
74 | ```
75 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | > A simple to use, highly customizable, and powerful modal for Angular Applications
7 |
8 | []()
9 | []()
10 | []()
11 | [](https://github.com/prettier/prettier)
12 | [](#contributors-)
13 | [](https://github.com/ngneat/)
14 | []()
15 |
16 | ## Features
17 |
18 | ✅ TemplateRef/Component Support
19 | ✅ Dialog Guards Support
20 | ✅ Resizable
21 | ✅ Draggable
22 | ✅ Multiple Dialogs Support
23 | ✅ Customizable
24 |
25 | ## Installation
26 |
27 | `npm i @ngneat/dialog`
28 |
29 | ## Usage
30 |
31 | ### Using a Component
32 |
33 | First, create the component to be displayed in the modal:
34 |
35 | ```ts
36 | import { DialogService, DialogRef } from '@ngneat/dialog';
37 |
38 | interface Data {
39 | title: string;
40 | }
41 |
42 | @Component({
43 | template: `
44 | {{ title }}
45 | Close
46 | `,
47 | standalone: true,
48 | changeDetection: ChangeDetectionStrategy.OnPush,
49 | })
50 | export class HelloWorldComponent {
51 | ref: DialogRef = inject(DialogRef);
52 |
53 | get title() {
54 | if (!this.ref.data) return 'Hello world';
55 | return this.ref.data.title;
56 | }
57 | }
58 | ```
59 |
60 | Inside the component, you'll have access to a `DialogRef` provider. You can call its `close()` method to close the current modal. You can also pass `data` that'll be available for any subscribers to `afterClosed$`.
61 |
62 | > 💡 Tip
63 | >
64 | > A publicly accessible property of type `DialogRef ` on your component will be used to infer the input and output types of your component.
65 |
66 | Now we can use the `DialogService` to open the modal and display the component:
67 |
68 | ```ts
69 | import { DialogService } from '@ngneat/dialog';
70 |
71 | @Component({
72 | standalone: true,
73 | template: ` Open `,
74 | })
75 | export class AppComponent implements OnInit {
76 | private dialog = inject(DialogService);
77 |
78 | ngOnInit() {
79 | const dialogRef = this.dialog.open(HelloWorldComponent, {
80 | // data is typed based on the passed generic
81 | data: {
82 | title: '',
83 | },
84 | });
85 | }
86 | }
87 | ```
88 |
89 | ### DialogRef API
90 |
91 | The `DialogRef` instance exposes the following API:
92 |
93 | - `afterClosed$` - An observable that emits after the modal closes:
94 |
95 | ```ts
96 | const dialogRef = this.dialog.open(HelloWorldComponent);
97 | dialogRef.afterClosed$.subscribe((result) => {
98 | console.log(`After dialog has been closed ${result}`);
99 | });
100 | ```
101 |
102 | - `backdropClick$` - An observable that emits when the user clicks on the modal backdrop:
103 |
104 | ```ts
105 | const dialogRef = this.dialog.open(HelloWorldComponent);
106 | dialogRef.backdropClick$.subscribe(() => {
107 | console.log('Backdrop has been clicked');
108 | });
109 | ```
110 |
111 | - `resetDrag` - A method that can be called to reset the dragged modal to the middle of the screen. An offset can be given as the first parameter to position it different from the center:
112 |
113 | ```ts
114 | dialogRef.resetDrag();
115 | dialogRef.resetDrag({ x: 100, y: 0 });
116 | ```
117 |
118 | - `beforeClose` - A guard that should return a `boolean`, an `observable`, or a `promise` indicating whether the modal can be closed:
119 |
120 | ```ts
121 | dialogRef.beforeClose((result) => dialogCanBeClosed);
122 | dialogRef.beforeClose((result) => this.service.someMethod(result));
123 | ```
124 |
125 | - `ref.data` - A reference to the `data` that is passed by the component opened in the modal:
126 |
127 | ```ts
128 | import { DialogService, DialogRef } from '@ngneat/dialog';
129 |
130 | @Component({
131 | template: `
132 | {{ ref.data.title }}
133 | Close
134 | `,
135 | standalone: true,
136 | changeDetection: ChangeDetectionStrategy.OnPush,
137 | })
138 | export class HelloWorldComponent {
139 | ref: DialogRef = inject(DialogRef);
140 | }
141 | ```
142 |
143 | - `ref.updateConfig` - An update function for the config, a common use case would be a reusable component setting its own common properties:
144 |
145 | ```ts
146 | import { DialogService, DialogRef } from '@ngneat/dialog';
147 |
148 | @Component({
149 | template: `
150 | {{ ref.data.title }}
151 | Close
152 | `,
153 | standalone: true,
154 | changeDetection: ChangeDetectionStrategy.OnPush,
155 | })
156 | export class MyVeryCommonDialogComponent {
157 | ref: DialogRef = inject(DialogRef);
158 |
159 | constructor() {
160 | this.ref.updateConfig({
161 | height: '200px',
162 | width: '400px',
163 | });
164 | }
165 | }
166 | ```
167 |
168 | > You can only update the config before the dialog is opened in the component's constructor.
169 |
170 | The library also provides the `dialogClose` directive helper, that you can use to close the modal:
171 |
172 | ```ts
173 | import { DialogService, DialogCloseDirective } from '@ngneat/dialog';
174 |
175 | @Component({
176 | standalone: true,
177 | imports: [DialogCloseDirective],
178 | template: `
179 | Hello World
180 | Close
181 | Close with result
182 | `,
183 | })
184 | export class HelloWorldComponent {}
185 | ```
186 |
187 | ### Using a TemplateRef
188 |
189 | Sometimes it can be overkill to create a whole component. In these cases, you can pass a reference to an ``:
190 |
191 | ```ts
192 | import { DialogService } from '@ngneat/dialog';
193 |
194 | @Component({
195 | selector: 'app-root',
196 | standalone: true,
197 | template: `
198 |
199 | Hello World
200 |
201 | Close
202 |
203 |
204 | Open
205 | `,
206 | })
207 | export class AppComponent {
208 | private dialog = inject(DialogService);
209 |
210 | open(tpl: TemplateRef) {
211 | this.dialog.open(tpl);
212 | }
213 | }
214 | ```
215 |
216 | Note that in this case, you can access the `ref` object by using the `$implicit` context property.
217 |
218 | ### Passing Data to the Modal Component
219 |
220 | Sometimes we need to pass data from the opening component to our modal component. In these cases, we can use the `data` property, and use it to pass any data we need:
221 |
222 | ```ts
223 | import { DialogService } from '@ngneat/dialog';
224 |
225 | @Component({
226 | standalone: true,
227 | template: ` Open `,
228 | })
229 | export class AppComponent implements OnInit {
230 | private dialog = inject(DialogService);
231 | private title = 'Dialog title';
232 |
233 | open() {
234 | const dialogRef = this.dialog.open(HelloWorldComponent, {
235 | data: {
236 | title: this.title,
237 | },
238 | });
239 | }
240 | }
241 | ```
242 |
243 | Now we can access it inside our modal component or template, by using the `ref.data` property.
244 |
245 | ## Dialog Options
246 |
247 | ### Global Options
248 |
249 | In the `forRoot` method when importing the dialog module in the app module you can specify the following options that will be globally applied to all dialog instances.
250 |
251 | - `closeButton` - Whether to display an 'X' for closing the modal (default is true).
252 | - `enableClose` - Whether a click on the backdrop, or press of the escape button, should close the modal (default is true), see [enable close](#enable-close).
253 | - `backdrop` - Whether to show the backdrop element (default is true).
254 | - `resizable` - Whether the modal show be resizeable (default is false).
255 | - `draggable` - Whether the modal show be draggable (default is false).
256 | - `draggableConstraint` - When draggable true, whether the modal should be constraint to the window. Use `none` for no constraint, `bounce` to have the modal bounce after it is released and `constrain` to constrain while dragging (default is `none`).
257 | - `size` - Set the modal size according to your global [custom sizes](#custom-sizes) (default is `md`).
258 | - `windowClass` - Add a custom class to the modal container.
259 | - `width` - Set a custom width (default unit is `px`).
260 | - `minWidth` - Set a custom min-width (default unit is `px`).
261 | - `maxWidth` - Set a custom max-width (default unit is `px`).
262 | - `height` - Set a custom height (default unit is `px`).
263 | - `minHeight` - Set a custom min-height (default unit is `px`).
264 | - `maxHeight` - Set a custom max-height (default unit is `px`).
265 | - `container` - A custom element to which we append the modal (default is `body`).
266 |
267 | ```ts
268 | import { provideDialogConfig } from '@ngneat/dialog';
269 |
270 | bootstrapApplication(AppComponent, {
271 | providers: [
272 | provideDialogConfig({
273 | closeButton: boolean,
274 | enableClose:
275 | boolean |
276 | 'onlyLastStrategy' |
277 | {
278 | escape: boolean | 'onlyLastStrategy',
279 | backdrop: boolean | 'onlyLastStrategy',
280 | },
281 | backdrop: boolean,
282 | resizable: boolean,
283 | draggable: boolean,
284 | overflow: boolean,
285 | draggableConstraint: none | bounce | constrain,
286 | sizes,
287 | size: sm | md | lg | fullScreen | string,
288 | windowClass: string,
289 | width: string | number,
290 | minWidth: string | number,
291 | maxWidth: string | number,
292 | height: string | number,
293 | minHeight: string | number,
294 | maxHeight: string | number,
295 | }),
296 | ],
297 | });
298 | ```
299 |
300 | ### Instance Options
301 |
302 | For each dialog instance you open you can specify all the global options and also the following 3 options.
303 |
304 | - `id` - The modal's unique id, the defaults are:
305 | - If a component is passed - the component's name (e.g. `MyCustomDialog`).
306 | - Otherwise, a random id is given.
307 | > [!Note]
308 | > while not required, it is recommended to set an id in order to prevent unwanted multiple instances of the same dialog.
309 | - `vcr` - A custom `ViewContainerRef` to use.
310 | - `data` - A `data` object that will be passed to the modal template or component.
311 |
312 | ```ts
313 | this.dialog.open(compOrTemplate, {
314 | //...
315 | // all global options expect sizes
316 | //...
317 | id: string,
318 | vcr: ViewContainerRef,
319 | data: {},
320 | });
321 | ```
322 |
323 | ### Enable close
324 |
325 | The `enableClose` property can be configured for each dialog.
326 | It can either be an object with the keys `escape` and `backdrop` for more granular control,
327 | or one of the values described below directly.
328 | The latter will apply the set value to both close triggers (escape and backdrop).
329 |
330 | If set to `true`, clicking on the backdrop or pressing the escape key will close the modal.
331 | If set to `false`, this behavior will be disabled.
332 |
333 | Additionally, the property can be set to the string value `'onlyLastStrategy'`.
334 | In this case, the behavior will only apply to the last dialog that was opened, and not to any other dialog.
335 | By default, this should be the top-most dialog and behave as `true`.
336 |
337 | ## Custom Sizes
338 |
339 | The default `sizes` config is:
340 |
341 | ```ts
342 | {
343 | sizes: {
344 | sm: {
345 | height: 'auto',
346 | width: '400px',
347 | },
348 | md: {
349 | height: 'auto',
350 | width: '560px',
351 | },
352 | lg: {
353 | height: 'auto',
354 | width: '800px',
355 | },
356 | fullScreen: {
357 | height: '100%',
358 | width: '100%',
359 | },
360 | }
361 | }
362 | ```
363 |
364 | You can override it globally by using the `sizes` option:
365 |
366 | ```ts
367 | bootstrapApplication(AppComponent, {
368 | providers: [
369 | provideDialogConfig({
370 | sizes: {
371 | sm: {
372 | width: 300, // 300px
373 | minHeight: 250, // 250px
374 | },
375 | md: {
376 | width: '60vw',
377 | height: '60vh',
378 | },
379 | lg: {
380 | width: '90vw',
381 | height: '90vh',
382 | },
383 | fullScreen: {
384 | width: '100vw',
385 | height: '100vh',
386 | },
387 | },
388 | }),
389 | ],
390 | });
391 | ```
392 |
393 | ## Styling
394 |
395 | You can customize the styles with these classes:
396 |
397 | ```scss
398 | ngneat-dialog {
399 | .ngneat-dialog-backdrop {
400 | // backdrop styles
401 | .ngneat-dialog-content {
402 | // dialog content, where your component/template is placed
403 | .ngneat-drag-marker {
404 | // draggable marker
405 | }
406 | .ngneat-close-dialog {
407 | // 'X' icon for closing the dialog
408 | }
409 | }
410 | }
411 | }
412 | ```
413 |
--------------------------------------------------------------------------------
/apps/playground/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts"],
7 | "extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"],
8 | "rules": {
9 | "@typescript-eslint/no-explicit-any": "off",
10 | "@angular-eslint/directive-selector": [
11 | "error",
12 | {
13 | "type": "attribute",
14 | "prefix": "app",
15 | "style": "camelCase"
16 | }
17 | ],
18 | "@angular-eslint/component-selector": [
19 | "error",
20 | {
21 | "type": "element",
22 | "prefix": "app",
23 | "style": "kebab-case"
24 | }
25 | ]
26 | }
27 | },
28 | {
29 | "files": ["*.html"],
30 | "extends": ["plugin:@nx/angular-template"],
31 | "rules": {}
32 | }
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/apps/playground/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/dialog'),
20 | reports: ['html', 'lcovonly', 'text-summary'],
21 | fixWebpackSourcePaths: true,
22 | },
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ['Chrome'],
29 | singleRun: false,
30 | restartOnFileChange: true,
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/apps/playground/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playground",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "projectType": "application",
5 | "generators": {
6 | "@schematics/angular:component": {
7 | "style": "scss"
8 | }
9 | },
10 | "sourceRoot": "apps/playground/src",
11 | "prefix": "app",
12 | "targets": {
13 | "build": {
14 | "executor": "@angular-devkit/build-angular:browser",
15 | "options": {
16 | "outputPath": "dist/apps/playground",
17 | "index": "apps/playground/src/index.html",
18 | "main": "apps/playground/src/main.ts",
19 | "polyfills": "apps/playground/src/polyfills.ts",
20 | "tsConfig": "apps/playground/tsconfig.app.json",
21 | "assets": ["apps/playground/src/favicon.svg", "apps/playground/src/assets"],
22 | "styles": ["apps/playground/src/styles.scss"],
23 | "scripts": [],
24 | "vendorChunk": true,
25 | "extractLicenses": false,
26 | "buildOptimizer": false,
27 | "sourceMap": true,
28 | "optimization": false,
29 | "namedChunks": true
30 | },
31 | "configurations": {
32 | "production": {
33 | "fileReplacements": [
34 | {
35 | "replace": "apps/playground/src/environments/environment.ts",
36 | "with": "apps/playground/src/environments/environment.prod.ts"
37 | }
38 | ],
39 | "optimization": true,
40 | "outputHashing": "all",
41 | "sourceMap": false,
42 | "namedChunks": false,
43 | "extractLicenses": true,
44 | "vendorChunk": false,
45 | "buildOptimizer": true,
46 | "budgets": [
47 | {
48 | "type": "initial",
49 | "maximumWarning": "2mb",
50 | "maximumError": "5mb"
51 | },
52 | {
53 | "type": "anyComponentStyle",
54 | "maximumWarning": "6kb",
55 | "maximumError": "10kb"
56 | }
57 | ]
58 | }
59 | },
60 | "defaultConfiguration": ""
61 | },
62 | "serve": {
63 | "executor": "@angular-devkit/build-angular:dev-server",
64 | "options": {
65 | "browserTarget": "playground:build"
66 | },
67 | "configurations": {
68 | "production": {
69 | "browserTarget": "playground:build:production"
70 | }
71 | }
72 | },
73 | "extract-i18n": {
74 | "executor": "@angular-devkit/build-angular:extract-i18n",
75 | "options": {
76 | "browserTarget": "playground:build"
77 | }
78 | },
79 | "lint": {
80 | "executor": "@nx/linter:eslint",
81 | "options": {
82 | "lintFilePatterns": ["apps/playground/src/**/*.ts", "apps/playground/src/**/*.html"]
83 | },
84 | "outputs": ["{options.outputFile}"]
85 | },
86 | "test": {
87 | "executor": "@angular-devkit/build-angular:karma",
88 | "options": {
89 | "main": "apps/playground/src/test.ts",
90 | "polyfills": "apps/playground/src/polyfills.ts",
91 | "tsConfig": "apps/playground/tsconfig.spec.json",
92 | "karmaConfig": "apps/playground/karma.conf.js",
93 | "assets": ["apps/playground/src/favicon.ico", "apps/playground/src/assets"],
94 | "styles": ["apps/playground/src/styles.scss"],
95 | "scripts": []
96 | }
97 | },
98 | "deploy": {
99 | "executor": "angular-cli-ghpages:deploy",
100 | "options": {}
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/apps/playground/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Test dialog using a custom template
4 |
This dialog has been opened with this custom config
5 |
{{ cleanConfig | json }}
6 |
NOTE: You can get the current config of the dialog using the config template variable
7 |
8 |
Click me to close the dialog 😉
9 |
10 |
11 |
12 |
13 |
Configure your dialog
14 |
75 |
76 |
You've clicked the backdrop 👏
77 |
78 |
79 |
80 | Open dialog using an ng-template!
81 | Open dialog using a component!
82 |
83 |
84 | 📢 This configuration is used in all the dialogs you open ☝️
85 |
86 |
87 |
📢 The body or title can be a string or a TemplateRef (ng-template)
88 |
📢 You can override the built-in dialogs with your own
89 |
90 |
91 |
92 |
Use a custom view container
93 |
94 |
95 | This is only a timer {{ (timer$ | async) || 0 }} ⏰
96 |
97 |
98 |
99 |
100 |
101 |
102 |
This dialog isn't attached to the ApplicationRef. It's attached to a custom ViewContainerRef.
103 |
104 |
You can take control of this view (this dialog) and, for example, dettach it from the change detection.
105 |
106 |
107 | When you click Toggle button, this timer (same timer of main page) will stop:
108 | {{ timer$ | async }}
109 |
110 |
Click on Detect changes to run a change detection or click again to reattach it
111 |
112 |
Toggle
113 |
Detect changes into template
114 |
Close
115 |
116 |
117 |
118 |
119 | Open the dialog with a custom View Container Ref
120 |
121 |
122 |
123 |
124 |
Reset location of a modal
125 |
126 | Open a dialog that you reset the location of
127 |
128 |
129 |
130 |
Pass data to the dialog and get a result
131 |
You can pass any data to your dialog:
132 |
142 |
143 |
144 | Your last result from the dialog was: {{ messageFromDialog || 'nothing 😢' }}
145 |
146 |
147 |
Open
148 |
149 |
--------------------------------------------------------------------------------
/apps/playground/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 | .note {
2 | background-color: #ffffcc;
3 | padding: 16px 32px;
4 | }
5 |
6 | .dialog {
7 | padding: 18px;
8 | }
9 |
10 | label {
11 | padding: 0em 0.75em;
12 | width: 200px;
13 | display: inline-block;
14 | }
15 |
16 | input,
17 | select {
18 | padding: 0.5em 0.75em;
19 | width: 200px;
20 | }
21 |
22 | button {
23 | margin-right: 10px;
24 | }
25 |
26 | form {
27 | max-height: 300px;
28 | display: flex;
29 | flex-direction: column;
30 | flex-wrap: wrap;
31 | align-content: flex-start;
32 | }
33 |
34 | form > div {
35 | display: flex;
36 | align-items: center;
37 | margin-bottom: 10px;
38 | height: 32px;
39 | }
40 |
--------------------------------------------------------------------------------
/apps/playground/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, TemplateRef, Type, ViewChild, ViewContainerRef } from '@angular/core';
2 | import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms';
3 | import { interval } from 'rxjs';
4 | import { shareReplay } from 'rxjs/operators';
5 | import { CommonModule } from '@angular/common';
6 |
7 | import { DialogCloseDirective, DialogService, DialogConfig } from '@ngneat/dialog';
8 |
9 | import { ResetLocationDialogComponent } from './reset-location-dialog.component';
10 | import { TestDialogComponent } from './test-dialog.component';
11 |
12 | @Component({
13 | selector: 'app-root',
14 | templateUrl: './app.component.html',
15 | imports: [ReactiveFormsModule, CommonModule, DialogCloseDirective],
16 | standalone: true,
17 | styleUrls: ['./app.component.scss'],
18 | })
19 | export class AppComponent {
20 | @ViewChild('template', { static: true })
21 | template: TemplateRef;
22 |
23 | @ViewChild('vcr', { static: true, read: ViewContainerRef })
24 | vcr: ViewContainerRef;
25 |
26 | component = TestDialogComponent;
27 |
28 | config = this.fb.group({
29 | id: [''],
30 | height: [''],
31 | width: [''],
32 | enableClose: this.fb.group({
33 | escape: [true],
34 | backdrop: [true],
35 | }),
36 | closeButton: [true],
37 | backdrop: [true],
38 | resizable: [false],
39 | draggable: [false],
40 | overflow: [true],
41 | dragConstraint: ['none'],
42 | size: [''],
43 | windowClass: [''],
44 | } as Partial>);
45 |
46 | builtIn = this.fb.group({
47 | type: ['confirm'],
48 | title: ['Test dialog 🔨'],
49 | body: ['This is the body for my dialog and it can contain HTML !'],
50 | });
51 |
52 | data = this.fb.group({
53 | title: ['My awesone custom title! :-D'],
54 | withResult: [true],
55 | });
56 |
57 | cleanConfig: Partial;
58 |
59 | result: any;
60 |
61 | messageFromDialog: string;
62 |
63 | closeOnce = false;
64 |
65 | templateOfCustomVCRIsAttached = true;
66 |
67 | timer$ = interval(1000).pipe(shareReplay(1));
68 |
69 | backDropClicked = false;
70 |
71 | constructor(
72 | private fb: UntypedFormBuilder,
73 | public dialog: DialogService,
74 | ) {}
75 |
76 | openDialog(compOrTemplate: Type | TemplateRef, config: DialogConfig) {
77 | this.backDropClicked = false;
78 | this.cleanConfig = this.normalizeConfig(config);
79 |
80 | if (this.cleanConfig.overflow) document.body.style.setProperty('--dialog-overflow', 'visible');
81 | else document.body.style.setProperty('--dialog-overflow', 'hidden');
82 |
83 | const ref = this.dialog.open(compOrTemplate as any, this.cleanConfig);
84 |
85 | ref?.backdropClick$.subscribe({
86 | next: () => (this.backDropClicked = true),
87 | });
88 |
89 | return ref;
90 | }
91 |
92 | openDialogWithCustomVCR(compOrTemplate: Type | TemplateRef, config: DialogConfig) {
93 | this.templateOfCustomVCRIsAttached = true;
94 |
95 | this.openDialog(compOrTemplate, {
96 | ...this.normalizeConfig(config),
97 | vcr: this.vcr,
98 | });
99 | }
100 |
101 | toggleDialogFromVCR() {
102 | const view = this.vcr.get(0);
103 |
104 | if (this.templateOfCustomVCRIsAttached) {
105 | view.detach();
106 | } else {
107 | view.reattach();
108 | }
109 |
110 | this.templateOfCustomVCRIsAttached = !this.templateOfCustomVCRIsAttached;
111 | }
112 |
113 | detectChangesIntoDialog() {
114 | const view = this.vcr.get(0);
115 | view.detectChanges();
116 | }
117 |
118 | openResetLocationDialog(config: DialogConfig) {
119 | this.openDialog(ResetLocationDialogComponent, { ...config, draggable: true });
120 | }
121 |
122 | openDialogWithCustomData(compOrTemplate: Type | TemplateRef, data: object, config: DialogConfig) {
123 | this.openDialog(compOrTemplate, {
124 | ...config,
125 | data,
126 | })?.afterClosed$.subscribe({
127 | next: (message?: string) => {
128 | if (typeof message === 'string') {
129 | this.messageFromDialog = message;
130 | }
131 | },
132 | });
133 | }
134 |
135 | private normalizeConfig(config: Partial): any {
136 | return Object.entries(config).reduce((cleanConfig, [key, value]) => {
137 | if (value != null && value !== '') {
138 | cleanConfig[key] = value;
139 | }
140 | return cleanConfig;
141 | }, {});
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/apps/playground/src/app/reset-location-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
2 | import { ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
3 |
4 | import { DialogRef } from '@ngneat/dialog';
5 |
6 | interface DialogData {
7 | title: string;
8 | withResult: boolean;
9 | }
10 |
11 | @Component({
12 | selector: 'app-reset-location-dialog',
13 | standalone: true,
14 | imports: [ReactiveFormsModule],
15 | template: `
16 | You can reset locations
17 |
18 |
19 |
20 | Offset-x
21 |
22 |
23 |
24 | Offset-y
25 |
26 |
27 |
28 |
29 | Reset location
30 | Close
31 |
32 |
33 | `,
34 | styles: [
35 | `
36 | .content {
37 | padding: 18px;
38 | padding-top: 0;
39 | }
40 |
41 | h2 {
42 | border-bottom: 1px solid black;
43 | padding: 18px;
44 | }
45 |
46 | .buttons {
47 | text-align: right;
48 | }
49 | `,
50 | ],
51 | changeDetection: ChangeDetectionStrategy.OnPush,
52 | })
53 | export class ResetLocationDialogComponent {
54 | offsetX = new UntypedFormControl(0);
55 | offsetY = new UntypedFormControl(0);
56 | ref: DialogRef = inject(DialogRef);
57 |
58 | resetDrag() {
59 | this.ref.resetDrag({ x: this.offsetX.value, y: this.offsetY.value });
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/apps/playground/src/app/test-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
2 | import { interval } from 'rxjs';
3 | import { ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
4 | import { CommonModule } from '@angular/common';
5 |
6 | import { DialogRef } from '@ngneat/dialog';
7 |
8 | interface DialogData {
9 | title: string;
10 | withResult: boolean;
11 | }
12 |
13 | @Component({
14 | selector: 'app-test-dialog',
15 | standalone: true,
16 | imports: [ReactiveFormsModule, CommonModule],
17 | template: `
18 | {{ ref.data?.title || 'Test dialog using a component' }}
19 |
20 |
21 |
This is a test dialog with a timer: {{ timer$ | async }}
22 |
23 |
24 | What is the message you want to return on close?
25 |
26 |
27 |
28 |
29 |
30 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet placerat erat, ac suscipit nisl.
31 | Phasellus euismod massa id leo facilisis eleifend. Sed bibendum pharetra molestie. Cras a odio lorem. Donec
32 | tellus ipsum, consectetur vel tempor at, gravida at ligula. Nullam cursus tempor nisl, nec interdum velit
33 | tempus eu. In semper venenatis augue, at porttitor tortor dictum ut. Praesent ut risus non lacus cursus
34 | consequat id sed ligula.
35 |
36 |
37 |
38 |
39 | {{ ref.data?.withResult ? 'Send your message' : 'Close' }}
40 |
41 |
42 | `,
43 | styles: [
44 | `
45 | .content {
46 | padding: 18px;
47 | padding-top: 0;
48 | }
49 |
50 | h2 {
51 | border-bottom: 1px solid black;
52 | padding: 18px;
53 | }
54 |
55 | .buttons {
56 | text-align: right;
57 | }
58 | `,
59 | ],
60 | changeDetection: ChangeDetectionStrategy.OnPush,
61 | })
62 | export class TestDialogComponent {
63 | timer$ = interval(1000);
64 | message = new UntypedFormControl('This dialog looks pretty cool 😎');
65 | ref: DialogRef = inject(DialogRef);
66 | }
67 |
--------------------------------------------------------------------------------
/apps/playground/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngneat/dialog/0153edfd31903b3a0076be89e27de0edbf9b48f5/apps/playground/src/assets/.gitkeep
--------------------------------------------------------------------------------
/apps/playground/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | };
4 |
--------------------------------------------------------------------------------
/apps/playground/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false,
7 | };
8 |
9 | /*
10 | * For easier debugging in development mode, you can import the following file
11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
12 | *
13 | * This import should be commented out in production mode because it will have a negative impact
14 | * on performance if an error is thrown.
15 | */
16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
17 |
--------------------------------------------------------------------------------
/apps/playground/src/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/playground/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Ngneat | Dialog Playground
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/apps/playground/src/main.ts:
--------------------------------------------------------------------------------
1 | import { bootstrapApplication } from '@angular/platform-browser';
2 |
3 | import { provideDialogConfig } from '@ngneat/dialog';
4 |
5 | import { AppComponent } from './app/app.component';
6 |
7 | let zIndex = 100;
8 |
9 | bootstrapApplication(AppComponent, {
10 | providers: [
11 | provideDialogConfig({
12 | zIndexGetter() {
13 | return zIndex++;
14 | },
15 | onOpen() {
16 | console.log('open');
17 | },
18 | onClose() {
19 | console.log('close');
20 | },
21 | }),
22 | ],
23 | });
24 |
--------------------------------------------------------------------------------
/apps/playground/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 | /**
22 | * By default, zone.js will patch all possible macroTask and DomEvents
23 | * user can disable parts of macroTask/DomEvents patch by setting following flags
24 | * because those flags need to be set before `zone.js` being loaded, and webpack
25 | * will put import in the top of bundle, so user need to create a separate file
26 | * in this directory (for example: zone-flags.ts), and put the following flags
27 | * into that file, and then add the following code before importing zone.js.
28 | * import './zone-flags';
29 | *
30 | * The flags allowed in zone-flags.ts are listed here.
31 | *
32 | * The following flags will work for all browsers.
33 | *
34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
37 | *
38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
40 | *
41 | * (window as any).__Zone_enable_cross_context_check = true;
42 | *
43 | */
44 |
45 | /***************************************************************************************************
46 | * Zone JS is required by default for Angular itself.
47 | */
48 | import 'zone.js'; // Included with Angular CLI.
49 |
50 | /***************************************************************************************************
51 | * APPLICATION IMPORTS
52 | */
53 |
--------------------------------------------------------------------------------
/apps/playground/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 | * {
3 | box-sizing: border-box;
4 | font-family: sans-serif;
5 | }
6 |
7 | body {
8 | max-width: 90%;
9 | margin: auto;
10 | }
11 |
12 | .block {
13 | margin-top: 40px;
14 | }
15 |
16 | .inline-block {
17 | display: inline-block;
18 | }
19 |
--------------------------------------------------------------------------------
/apps/playground/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/testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
6 |
7 | // First, initialize the Angular testing environment.
8 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
9 | teardown: { destroyAfterEach: false },
10 | });
11 |
--------------------------------------------------------------------------------
/apps/playground/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": []
6 | },
7 | "files": ["src/main.ts", "src/polyfills.ts"],
8 | "include": ["src/**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/apps/playground/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": ["jasmine", "node"]
6 | },
7 | "files": ["src/test.ts", "src/polyfills.ts"],
8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/changelog.config.js:
--------------------------------------------------------------------------------
1 | const types = {
2 | feat: {
3 | description: 'A new feature',
4 | emoji: '🎸',
5 | value: 'feat',
6 | },
7 | fix: {
8 | description: 'A bug fix',
9 | emoji: '🐛',
10 | value: 'fix',
11 | },
12 | test: {
13 | description: 'Adding missing tests',
14 | emoji: '💍',
15 | value: 'test',
16 | },
17 | chore: {
18 | description: 'Build process or auxiliary tool changes',
19 | emoji: '🤖',
20 | value: 'chore',
21 | },
22 | docs: {
23 | description: 'Documentation only changes',
24 | emoji: '✏️',
25 | value: 'docs',
26 | },
27 | refactor: {
28 | description: 'A code change that neither fixes a bug or adds a feature',
29 | emoji: '💡',
30 | value: 'refactor',
31 | },
32 | ci: {
33 | description: 'CI related changes',
34 | emoji: '🎡',
35 | value: 'ci',
36 | },
37 | style: {
38 | description: 'Markup, white-space, formatting, missing semi-colons...',
39 | emoji: '💄',
40 | value: 'style',
41 | },
42 | };
43 |
44 | module.exports = {
45 | disableEmoji: false,
46 | list: Object.keys(types),
47 | maxMessageLength: 64,
48 | minMessageLength: 3,
49 | questions: ['type', 'scope', 'subject', 'body', 'breaking', 'issues'],
50 | types,
51 | };
52 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | };
4 |
--------------------------------------------------------------------------------
/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 | const { join } = require('path');
5 | const { constants } = require('karma');
6 |
7 | module.exports = () => {
8 | return {
9 | basePath: '',
10 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
11 | plugins: [
12 | require('karma-jasmine'),
13 | require('karma-chrome-launcher'),
14 | require('karma-jasmine-html-reporter'),
15 | require('karma-coverage'),
16 | require('@angular-devkit/build-angular/plugins/karma'),
17 | ],
18 | client: {
19 | jasmine: {
20 | // you can add configuration options for Jasmine here
21 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
22 | // for example, you can disable the random execution with `random: false`
23 | // or set a specific seed with `seed: 4321`
24 | },
25 | clearContext: false, // leave Jasmine Spec Runner output visible in browser
26 | },
27 | jasmineHtmlReporter: {
28 | suppressAll: true, // removes the duplicated traces
29 | },
30 | coverageReporter: {
31 | dir: join(__dirname, './coverage'),
32 | subdir: '.',
33 | reporters: [{ type: 'html' }, { type: 'text-summary' }],
34 | },
35 | reporters: ['progress', 'kjhtml'],
36 | port: 9876,
37 | colors: true,
38 | logLevel: constants.LOG_INFO,
39 | autoWatch: true,
40 | browsers: ['Chrome'],
41 | singleRun: true,
42 | };
43 | };
44 |
--------------------------------------------------------------------------------
/libs/dialog/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts"],
7 | "excludedFiles": ["*.spec.ts"],
8 | "extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"],
9 | "rules": {
10 | "@typescript-eslint/no-explicit-any": "off",
11 | "@typescript-eslint/no-this-alias": "off",
12 | "@typescript-eslint/no-non-null-assertion": "off",
13 | "@angular-eslint/no-input-rename": "off"
14 | }
15 | },
16 | {
17 | "files": ["types.spec.ts"],
18 | "rules": {
19 | "@typescript-eslint/ban-types": "off",
20 | "@typescript-eslint/no-unused-vars": "off"
21 | }
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/libs/dialog/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4 |
5 | ## [5.1.2](/compare/v5.1.1...v5.1.2) (2024-12-02)
6 |
7 |
8 | ### Bug Fixes
9 |
10 | * prevent dialog from closing when selecting elements from the dialog 54908f2, closes #129
11 |
12 |
13 |
14 | ## [5.1.1](/compare/v5.1.0...v5.1.1) (2024-07-15)
15 |
16 |
17 | ### Bug Fixes
18 |
19 | * use component selector as default 3aee938, closes #123
20 |
21 |
22 |
23 | # [5.1.0](/compare/v5.0.0...v5.1.0) (2024-07-05)
24 |
25 |
26 | ### Bug Fixes
27 |
28 | * allow nullable values again 86f6605, closes #119 #81
29 |
30 |
31 | ### Features
32 |
33 | * overflow functionality c3a710f
34 |
35 |
36 |
37 | # [5.0.0](/compare/v4.1.1...v5.0.0) (2023-11-28)
38 |
39 |
40 | ### Features
41 |
42 | * 🎸 allow updating the config via dialog ref 9e33a61
43 | * 🎸 update to new control flow b827e4e
44 | * 🎸 update to ng17 9bf838c
45 |
46 |
47 | ### BREAKING CHANGES
48 |
49 | * 🧨 peer dep is angular 17
50 |
51 |
52 |
53 | # [4.2.0](https://github.com/ngneat/dialog/compare/v4.1.1...v4.2.0) (2023-11-13)
54 |
55 |
56 | ### Features
57 |
58 | * 🎸 allow updating the config via dialog ref ([9e33a61](https://github.com/ngneat/dialog/commit/9e33a6112a2a380c3291901b3ca70e0e25493489))
59 |
60 |
61 |
62 | ### [4.1.1](https://github.com/ngneat/dialog/compare/v4.1.0...v4.1.1) (2023-09-11)
63 |
64 | ### Bug Fixes
65 |
66 | - 🐛 ignore open dialogs instead of throwing an error ([6779fc4](https://github.com/ngneat/dialog/commit/6779fc4481e11ba205a612aa00c1456c888195f5))
67 |
68 | ## [4.1.0](https://github.com/ngneat/dialog/compare/v4.0.0...v4.1.0) (2023-08-02)
69 |
70 | ### Features
71 |
72 | - **config:** split up backdrop+escape enableClose ([5d8700d](https://github.com/ngneat/dialog/commit/5d8700d2e2231760718bbad7adc11d184b756edf)), closes [#34](https://github.com/ngneat/dialog/issues/34)
73 | - configure min-width and max-width sizes ([fb7dd0c](https://github.com/ngneat/dialog/commit/fb7dd0c10159a89d878c4c6390a0273565ad1aaa))
74 |
75 | ## [4.0.0](https://github.com/ngneat/dialog/compare/v3.6.0...v4.0.0) (2023-05-02)
76 |
77 | ### ⚠ BREAKING CHANGES
78 |
79 | - narrow result type definition
80 |
81 | * The `Result` generic is now infered based on the public ref property in the component
82 | * `DialogRef.afterClosed$` will now use the infered result type
83 | * `DialogRef.close` will now use the infered resut type
84 | * `DialogRef.beforeClose` guard will now use the infered result type
85 |
86 | ### Features
87 |
88 | - 🎸 add option to only enable close for last opened dialog ([dba14e8](https://github.com/ngneat/dialog/commit/dba14e835a35b8ec93320e9c22e8d113da8fa6e9)), closes [#96](https://github.com/ngneat/dialog/issues/96)
89 |
90 | ### Bug Fixes
91 |
92 | - expose return type of dialog ref ([91a30d6](https://github.com/ngneat/dialog/commit/91a30d6dde16706625e5b5f2f88da0d2f34903ff)), closes [#86](https://github.com/ngneat/dialog/issues/86)
93 |
94 | ## [3.6.0](https://github.com/ngneat/dialog/compare/v3.5.0...v3.6.0) (2023-04-24)
95 |
96 | ### Features
97 |
98 | - 🎸 add zIndex getter ([8d63395](https://github.com/ngneat/dialog/commit/8d63395afde5bf0b058138a10b594578ccf48f11))
99 |
100 | ## [3.5.0](https://github.com/ngneat/dialog/compare/v3.4.0...v3.5.0) (2023-02-26)
101 |
102 | ### Features
103 |
104 | - 🎸 add CloseAllDialogsDirective ([86704d0](https://github.com/ngneat/dialog/commit/86704d0fbbc057290a62025f00b92116c83df7be))
105 |
106 | ## [3.4.0](https://github.com/ngneat/dialog/compare/v3.1.0...v3.4.0) (2023-02-23)
107 |
108 | ### Features
109 |
110 | - 🎸 add css variables ([4e071aa](https://github.com/ngneat/dialog/commit/4e071aafaa51f65ea5bf2e6e818c1dc413a35547))
111 | - 🎸 add more variables ([1d7ca80](https://github.com/ngneat/dialog/commit/1d7ca80387f3f2abc67b057c6782a98023484011))
112 | - add aria role attribute to dialog component ([0733000](https://github.com/ngneat/dialog/commit/0733000065b4dcb3070411137f8cef56c3f24afe)), closes [#90](https://github.com/ngneat/dialog/issues/90)
113 |
114 | ### Bug Fixes
115 |
116 | - 🐛 remove redundant top ([f81a9b6](https://github.com/ngneat/dialog/commit/f81a9b6bedce1689d7b4ee069f0044de4d32aa67))
117 | - **draggable:** move target instead of handle ([490b28b](https://github.com/ngneat/dialog/commit/490b28b4e96d4f8f6da8696bce7b1ed8b3b87c8b)), closes [#84](https://github.com/ngneat/dialog/issues/84)
118 |
119 | ## [3.3.0](https://github.com/ngneat/dialog/compare/v3.2.1...v3.3.0) (2023-01-05)
120 |
121 | ### Features
122 |
123 | - 🎸 add more variables ([b2be840](https://github.com/ngneat/dialog/commit/b2be840f6d224c318d080d3f5f9c42484ffcc5f1))
124 |
125 | ### [3.2.1](https://github.com/ngneat/dialog/compare/v3.2.0...v3.2.1) (2023-01-05)
126 |
127 | ### Bug Fixes
128 |
129 | - 🐛 remove redundant top ([42638bb](https://github.com/ngneat/dialog/commit/42638bbfdaae30c0a66715e4ad80906b4733f1a8))
130 |
131 | ## [3.2.0](https://github.com/ngneat/dialog/compare/v3.1.0...v3.2.0) (2023-01-05)
132 |
133 | ### Features
134 |
135 | - 🎸 add css variables ([6a6c334](https://github.com/ngneat/dialog/commit/6a6c3346a1c0b2bd3441651c4c7cc71539062307))
136 |
137 | ## [3.1.0](https://github.com/ngneat/dialog/compare/v3.0.2...v3.1.0) (2023-01-01)
138 |
139 | ### Features
140 |
141 | - 🎸 expose dialog open statuses ([030a68d](https://github.com/ngneat/dialog/commit/030a68de9d3a4295a66e33e76d77b51555140b34))
142 |
143 | ### [3.0.2](https://github.com/ngneat/dialog/compare/v3.0.1...v3.0.2) (2022-12-12)
144 |
145 | ### Bug Fixes
146 |
147 | - 🐛 allow nullable values ([59cb603](https://github.com/ngneat/dialog/commit/59cb603d5b009678b7e6d0697b18d2717151355e)), closes [#81](https://github.com/ngneat/dialog/issues/81)
148 |
149 | ### [3.0.1](https://github.com/ngneat/dialog/compare/v2.1.1...v3.0.1) (2022-12-04)
150 |
151 | ### ⚠ BREAKING CHANGES
152 |
153 | - upgrade to standalone components
154 |
155 | * Remove `DialogModule`
156 | * Remove built-in dialogs
157 | * Sizes default `height` is now `auto`
158 | * Expose `provideDialogConfig` function
159 | * `dialogClose` should be imported when used
160 | * The `data` property type now inferred based on the public ref property in the component (see docs)
161 |
162 | ### Features
163 |
164 | - customize confirm and cancel button texts of built-in dialogs ([a7eab24](https://github.com/ngneat/dialog/commit/a7eab2423ad41bcb3e2a32ece9a332c5e3381812)), closes [#47](https://github.com/ngneat/dialog/issues/47) [#62](https://github.com/ngneat/dialog/issues/62)
165 | - upgrade to standalone components ([f5575f5](https://github.com/ngneat/dialog/commit/f5575f59da2fb61b9c0aa229e8890eaf32a2a56c))
166 |
167 | ### Bug Fixes
168 |
169 | - 🐛 fix types ([27c2621](https://github.com/ngneat/dialog/commit/27c2621127286c5ae24f2b361e07ea20d02faa18))
170 | - add reset location dialog component for demo ([266a9eb](https://github.com/ngneat/dialog/commit/266a9ebd30de438a3888fe3f558baa69768007b3))
171 |
172 | ### Build System
173 |
174 | - fix the build script and add build test to ci ([3f1f280](https://github.com/ngneat/dialog/commit/3f1f280e1610ce4da4fdc1b7b9a8c8a1c4122ddb))
175 |
176 | ## [3.0.0](https://github.com/ngneat/dialog/compare/v2.1.1...v3.0.0) (2022-11-24)
177 |
178 | ### ⚠ BREAKING CHANGES
179 |
180 | - upgrade to standalone components
181 |
182 | * Remove `DialogModule`
183 | * Remove built-in dialogs
184 | * Sizes default `height` is now `auto`
185 | * Expose `provideDialogConfig` function
186 | * `dialogClose` should be imported when used
187 | * The `data` property type now infered based on the public ref property in the component (see docs)
188 |
189 | ### Bug Fixes
190 |
191 | - add reset location dialog component for demo ([266a9eb](https://github.com/ngneat/dialog/commit/266a9ebd30de438a3888fe3f558baa69768007b3))
192 |
193 | ### Build System
194 |
195 | - fix the build script and add build test to ci ([3f1f280](https://github.com/ngneat/dialog/commit/3f1f280e1610ce4da4fdc1b7b9a8c8a1c4122ddb))
196 |
197 | ### [2.1.1](https://github.com/ngneat/dialog/compare/v2.1.0...v2.1.1) (2022-06-14)
198 |
199 | ### Bug Fixes
200 |
201 | - pass through offset parameters to resetDrag method ([2fde3d1](https://github.com/ngneat/dialog/commit/2fde3d14ebc30461254e85d91a485ee3f8ca8e23))
202 | - **dialog-service:** dialog invalid id ([2722393](https://github.com/ngneat/dialog/commit/2722393228016eb412e8638eaf731fe22e16d64c)), closes [#63](https://github.com/ngneat/dialog/issues/63)
203 |
204 | ## [2.1.0](https://github.com/ngneat/dialog/compare/v2.0.1...v2.1.0) (2022-03-31)
205 |
206 | ### Features
207 |
208 | - add dialog strict typings [#54](https://github.com/ngneat/dialog/issues/54) ([b011c9f](https://github.com/ngneat/dialog/commit/b011c9f986d7310a55efe256bc481ea67292f80f))
209 |
210 | ### Bug Fixes
211 |
212 | - fix dialog open config param default parameter ([207cdf2](https://github.com/ngneat/dialog/commit/207cdf2496cc6c8f16da52047165c58edef84337))
213 | - fix dialog service open return typings ([2e7b93f](https://github.com/ngneat/dialog/commit/2e7b93fe195b57933af0599266d77e28cadf5626))
214 |
215 | ### Tests
216 |
217 | - fix dialog.service.spec.ts type error ([3f9791c](https://github.com/ngneat/dialog/commit/3f9791c0fb874d267ad4f66c81c3c1201dab5692))
218 |
219 | ### [2.0.1](https://github.com/ngneat/dialog/compare/v2.0.0...v2.0.1) (2022-01-04)
220 |
221 | ### Bug Fixes
222 |
223 | - 🐛 clicking on element with ngIf closes the dialog ([490ab3d](https://github.com/ngneat/dialog/commit/490ab3dce0a7a0b0c6df0b17101b017588d887b6))
224 |
225 | ## [2.0.0](https://github.com/ngneat/dialog/compare/v1.7.0...v2.0.0) (2021-11-24)
226 |
227 | ### ⚠ BREAKING CHANGES
228 |
229 | - Peer dep of v13
230 |
231 | ### Features
232 |
233 | - 🎸 angular v13 ([b810c16](https://github.com/ngneat/dialog/commit/b810c16ff234a52be67a0165ec5b1d415a17ce6f))
234 |
235 | ## [1.7.0](https://github.com/ngneat/dialog/compare/v1.6.0...v1.7.0) (2021-10-18)
236 |
237 | ### Features
238 |
239 | - add drag constraint and reset methods ([feb39c4](https://github.com/ngneat/dialog/commit/feb39c4a4231834dd91bfbc9eb9fd16f33ee2a34)), closes [#36](https://github.com/ngneat/dialog/issues/36)
240 | - closeButton without backdrop click close ([94e52e8](https://github.com/ngneat/dialog/commit/94e52e8401e7b28c39b28e955bb378ec73ed150b))
241 | - move options to the global config ([0a0c5c6](https://github.com/ngneat/dialog/commit/0a0c5c68fddac185419d6b062f0a9cfa34944a58)), closes [#37](https://github.com/ngneat/dialog/issues/37)
242 |
243 | ## [1.6.0](https://github.com/ngneat/dialog/compare/v1.5.0...v1.6.0) (2021-03-21)
244 |
245 | ### Features
246 |
247 | - allow multiple classes to be set from windowClass ([5ac3297](https://github.com/ngneat/dialog/commit/5ac3297fb664c4cd104b00648ebe21f09df55e72))
248 | - allow multiple classes to be set from windowClass ([d60ee41](https://github.com/ngneat/dialog/commit/d60ee4142436137e2d0c6ce2f75848796e8b717b))
249 |
250 | ## [1.5.0](https://github.com/ngneat/dialog/compare/v1.4.1...v1.5.0) (2021-03-16)
251 |
252 | ### Features
253 |
254 | - add closeAll to dialog service ([fa8cb92](https://github.com/ngneat/dialog/commit/fa8cb9272ce0a3ec00209b1d2b085a991e18c261)), closes [#23](https://github.com/ngneat/dialog/issues/23)
255 |
256 | ### Bug Fixes
257 |
258 | - **dialog-service:** Remove 'ngneat-dialog-hidden' from body only after last dialog is closed ([83477dd](https://github.com/ngneat/dialog/commit/83477dd0d697d989eda2930214b265bd190b46e8)), closes [ngneat/dialog#26](https://github.com/ngneat/dialog/issues/26)
259 | - close dialog on backdrop click ([efbdb11](https://github.com/ngneat/dialog/commit/efbdb112b613240d8a97cf7be0aa6a4ca6700efb))
260 | - **schematics:** use correct folder ([6e52e31](https://github.com/ngneat/dialog/commit/6e52e312f2130be8f87003766c7d77aff6044b79))
261 |
262 | ### [1.4.1](https://github.com/ngneat/dialog/compare/v1.4.0...v1.4.1) (2021-02-02)
263 |
264 | ### Bug Fixes
265 |
266 | - 🐛 replace close icon ([b747695](https://github.com/ngneat/dialog/commit/b7476951280c4b0620557d5cbb8e0809fa271453))
267 |
268 | ## [1.4.0](https://github.com/ngneat/dialog/compare/v1.3.0...v1.4.0) (2021-01-26)
269 |
270 | ### Features
271 |
272 | - 🎸 support custom sizes ([66e90ad](https://github.com/ngneat/dialog/commit/66e90ad9a35247615b99489a2022ab18719b423d))
273 |
274 | ### Bug Fixes
275 |
276 | - 🐛 set deafult modal size to md ([fc9c79a](https://github.com/ngneat/dialog/commit/fc9c79ac8e8b1113750dee5676727f017356678d))
277 |
278 | ## [1.3.0](https://github.com/ngneat/dialog/compare/v1.0.4...v1.3.0) (2021-01-26)
279 |
280 | ### Features
281 |
282 | - 🎸 add max height to config ([d2f57ea](https://github.com/ngneat/dialog/commit/d2f57eabf6e6844c727b0ce81e2008c0cf93dd6b))
283 |
284 | ### Bug Fixes
285 |
286 | - **dialog-component:** use appendChild instead of append (IE) ([e4e56e2](https://github.com/ngneat/dialog/commit/e4e56e2b56b71b7656eb81d2b0e050cb762a24a7))
287 |
288 | ### [1.2.1](https://github.com/ngneat/dialog/compare/v1.2.0...v1.2.1) (2020-12-01)
289 |
290 | ### Bug Fixes
291 |
292 | - 🐛 fix position ([88442d7](https://github.com/ngneat/dialog/commit/88442d756aeb784b29ed29df0a5977c0fe012eb9))
293 |
294 | ## [1.2.0](https://github.com/ngneat/dialog/compare/v1.1.0...v1.2.0) (2020-12-01)
295 |
296 | ### Features
297 |
298 | - 🎸 add onopen and onclose to global config ([cfc68ba](https://github.com/ngneat/dialog/commit/cfc68ba18285a49c1771174f33dc3f2a507b645b))
299 |
300 | ### Bug Fixes
301 |
302 | - 🐛 change modal animation ([65c6d85](https://github.com/ngneat/dialog/commit/65c6d85f9a61f72fddede5b2c93c188738617277))
303 |
304 | ### [1.0.4](https://github.com/ngneat/dialog/compare/v1.1.1...v1.0.4) (2020-12-19)
305 |
306 | ### Bug Fixes
307 |
308 | - provide default value in forRoot() method ([4ccc22d](https://github.com/ngneat/dialog/commit/4ccc22d025dc6060272af7556f29dd84b7aac005))
309 |
310 | ### [1.0.3](https://github.com/ngneat/dialog/compare/v1.0.2...v1.0.3) (2020-11-05)
311 |
312 | ### Bug Fixes
313 |
314 | - 🐛 add dialog comp to entry ([3674346](https://github.com/ngneat/dialog/commit/3674346f3520ef127bed9e922297e62bce1c84da))
315 |
316 | ### [1.0.2](https://github.com/ngneat/dialog/compare/v1.0.1...v1.0.2) (2020-08-26)
317 |
318 | ### Bug Fixes
319 |
320 | - 🐛 expose config interfaces and config to dialogs ([8e31702](https://github.com/ngneat/dialog/commit/8e317021996c32dde6f1b93d7215d9041ab111ea)), closes [#7](https://github.com/ngneat/dialog/issues/7)
321 |
322 | ### [1.0.1](https://github.com/ngneat/dialog/compare/v1.0.0...v1.0.1) (2020-07-03)
323 |
324 | ### Bug Fixes
325 |
326 | - 🐛 fix backdrop style ([78babff](https://github.com/ngneat/dialog/commit/78babfffc813fd58270b1b370b0ffa5a6c944014))
327 |
328 | ## 1.0.0 (2020-06-30)
329 |
330 | ### Features
331 |
332 | - 🎸 add min height support ([f4c8f12](https://github.com/ngneat/dialog/commit/f4c8f1296320c04137ecf8d54940e0b2698c27f5))
333 | - 🎸 add x close button ([546adcc](https://github.com/ngneat/dialog/commit/546adcc84dcc24a0d32f678c13f9e0f86a597b8e))
334 | - add dialog playground ([31fa872](https://github.com/ngneat/dialog/commit/31fa8725d2712640d972fcb47e2e47c94006d403))
335 | - **built-ins:** add confirm, error and success ([24d4188](https://github.com/ngneat/dialog/commit/24d418852d4f3f01e3717357a84aadd5f708dc44))
336 | - **dialog-close:** add dialog-close directive ([28d3d49](https://github.com/ngneat/dialog/commit/28d3d494090cf2542bf61078f1d0ef7c3884c3d6))
337 | - **schematics:** ng-add schematic ([e2c3447](https://github.com/ngneat/dialog/commit/e2c344754b5664624de54fc16bb9a2a5567abef2))
338 | - 🎸 add dialog-service and most of features ([13b8be4](https://github.com/ngneat/dialog/commit/13b8be4079b63da2475589fd1e1f5a3b1630b940))
339 | - add beforeClose guards ([3c9333e](https://github.com/ngneat/dialog/commit/3c9333e7607fca7419970668844d9481e46c4074))
340 | - add dialog-cmp, add hooks and pass data ([97e822a](https://github.com/ngneat/dialog/commit/97e822a8c985dee18c5c0bc41f71e1ff5984a4a6))
341 | - add resizable option ([505b0f1](https://github.com/ngneat/dialog/commit/505b0f1fc4410fc9ca5e135d86e057e06265e117))
342 | - allow pass an ElementRef as container ([4718c20](https://github.com/ngneat/dialog/commit/4718c2080edfc1d060cdb8970e5e7548c1102bff))
343 | - **dialog-component:** add open animation ([2f5b9c8](https://github.com/ngneat/dialog/commit/2f5b9c88f0c591fed653652c5d52b2e9af3f1c2b))
344 | - global config, sizes, vcr and draggable ([7a1b4ca](https://github.com/ngneat/dialog/commit/7a1b4ca09c25209a27a39dd5eab5cb52ce09032a))
345 |
346 | ### Bug Fixes
347 |
348 | - 🐛 dialog styles ([a0dd1a3](https://github.com/ngneat/dialog/commit/a0dd1a31540e5e3d524c237ecefb5b6aed6aa20a))
349 | - backdropClick\$ is subscribable after open ([4b1e979](https://github.com/ngneat/dialog/commit/4b1e979f38ad0e5dd530fa9ff8f7b81fd52b68df))
350 | - fullScreen is a size ([a883c8a](https://github.com/ngneat/dialog/commit/a883c8adca44b3f6844abd43de05e3af9d8cac2a))
351 | - **built-ins:** add padding if there's title ([7868be7](https://github.com/ngneat/dialog/commit/7868be7af758b9afd1a62c8ae548a763ee88f92a))
352 | - **built-ins:** scope styles ([7eba7ce](https://github.com/ngneat/dialog/commit/7eba7ce8d5d7a874005baadf8f7f157bdb31737d))
353 | - **dialog-component:** bind right context ([4398651](https://github.com/ngneat/dialog/commit/43986513e34974c81f3eb86d8e84c2d91c6591a7))
354 | - **dialog-component:** hide backdrop ([9e056c7](https://github.com/ngneat/dialog/commit/9e056c75767f2e7c7a0b07c048436a79f026efa2))
355 | - **dialog-component:** set windowClass at host ([63ba90e](https://github.com/ngneat/dialog/commit/63ba90e3a9749deb2a1f5611b0c6096414949aa4))
356 | - **dialog-module:** forRoot config is optional ([e9c78d1](https://github.com/ngneat/dialog/commit/e9c78d146f228c66fd06d933c08e43f2616275e7))
357 | - **dialog-ref:** unwrap RefType ([2f23abf](https://github.com/ngneat/dialog/commit/2f23abf67643a391963d9b47ce53d948f18c2582))
358 | - **dialog-service:** add more-general overload ([eb94617](https://github.com/ngneat/dialog/commit/eb946179f46900eda505d5278523c88bfcf5826e))
359 | - **draggable-directive:** move marker on init ([25dc5dc](https://github.com/ngneat/dialog/commit/25dc5dc93f5e7eb7657c62b8b9678d9d53a93595))
360 | - afterClosed\$ should emit result ([4cf37f0](https://github.com/ngneat/dialog/commit/4cf37f0f8bb18aacad7b950f24ca2c936561e840))
361 | - clean-up only references ([4a37b26](https://github.com/ngneat/dialog/commit/4a37b26eea0045f3a25aa2f16ba61b68158e1b40))
362 | - dialog-service ([aa647b1](https://github.com/ngneat/dialog/commit/aa647b106151b749d44820c7448860c9a3f5d5d8))
363 | - export DIALOG_DATA token ([1b266b8](https://github.com/ngneat/dialog/commit/1b266b826bc936295c7c90a9c2094c3e40f2a795))
364 | - **dialog-module:** sizes are optional ([c8e851c](https://github.com/ngneat/dialog/commit/c8e851ce3f2fb18311b46efbcf74550f93dd6360))
365 | - set sizes from module ([6232b7c](https://github.com/ngneat/dialog/commit/6232b7cfdf52461d86bc528bdbd6b4e48e1356c2))
366 | - **dialog-service:** use vcr with template-ref ([a57660b](https://github.com/ngneat/dialog/commit/a57660b52d349edb2f0b2ee673de9c03f3c18b19))
367 |
368 | ### Tests
369 |
370 | - **dialog:** improve describe ([b64ef2e](https://github.com/ngneat/dialog/commit/b64ef2e36626f852833a07e8029c32436649d390))
371 | - add tests ([8ca14c4](https://github.com/ngneat/dialog/commit/8ca14c4465f325a281213d2829275808af567d2a))
372 | - missing expect ([6ddfe75](https://github.com/ngneat/dialog/commit/6ddfe75c62e46e2ff72f8923f08e4f4439d4634b))
373 |
--------------------------------------------------------------------------------
/libs/dialog/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/dialog'),
20 | reports: ['html', 'lcovonly', 'text-summary'],
21 | fixWebpackSourcePaths: true,
22 | },
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: [process.env.CI ? 'ChromeHeadless' : 'Chrome'],
29 | singleRun: process.env.CI,
30 | restartOnFileChange: true,
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/libs/dialog/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "../../dist/libs/dialog",
4 | "assets": ["CHANGELOG.md"],
5 | "lib": {
6 | "entryFile": "src/public-api.ts"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/libs/dialog/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ngneat/dialog",
3 | "version": "5.1.2",
4 | "description": "Simple to use, highly customizable, and powerful modal for Angular Apps",
5 | "dependencies": {
6 | "tslib": "2.3.1"
7 | },
8 | "peerDependencies": {
9 | "@angular/core": ">=17.0.0"
10 | },
11 | "keywords": [
12 | "angular",
13 | "dialog",
14 | "modal",
15 | "angular modal"
16 | ],
17 | "license": "MIT",
18 | "publishConfig": {
19 | "access": "public"
20 | },
21 | "bugs": {
22 | "url": "https://github.com/ngneat/dialog/issue"
23 | },
24 | "homepage": "https://github.com/ngneat/dialog#readme",
25 | "repository": {
26 | "type": "git",
27 | "url": "https://github.com/ngneat/dialog"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/libs/dialog/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dialog",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "projectType": "library",
5 | "sourceRoot": "libs/dialog/src",
6 | "prefix": "lib",
7 | "targets": {
8 | "build": {
9 | "executor": "@nx/angular:package",
10 | "options": {
11 | "tsConfig": "libs/dialog/tsconfig.lib.json",
12 | "project": "libs/dialog/ng-package.json"
13 | },
14 | "configurations": {
15 | "production": {
16 | "tsConfig": "libs/dialog/tsconfig.lib.prod.json"
17 | }
18 | }
19 | },
20 | "test": {
21 | "executor": "@angular-devkit/build-angular:karma",
22 | "options": {
23 | "main": "libs/dialog/src/test.ts",
24 | "tsConfig": "libs/dialog/tsconfig.spec.json",
25 | "karmaConfig": "libs/dialog/karma.conf.js"
26 | }
27 | },
28 | "lint": {
29 | "executor": "@nx/linter:eslint",
30 | "options": {
31 | "lintFilePatterns": ["libs/dialog/src/**/*.ts", "libs/dialog/src/**/*.html"]
32 | },
33 | "outputs": ["{options.outputFile}"]
34 | },
35 | "version": {
36 | "executor": "@jscutlery/semver:version",
37 | "options": {
38 | "tagPrefix": "v"
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/libs/dialog/src/lib/close-all-dialogs.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive, HostListener, inject } from '@angular/core';
2 |
3 | import { DialogService } from './dialog.service';
4 |
5 | @Directive({
6 | selector: '[closeAllDialogs]',
7 | standalone: true,
8 | })
9 | export class CloseAllDialogsDirective {
10 | private dialogService = inject(DialogService);
11 |
12 | @HostListener('click')
13 | onClose() {
14 | this.dialogService.closeAll();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/libs/dialog/src/lib/dialog-close.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive, ElementRef, HostListener, inject, Input, OnInit } from '@angular/core';
2 |
3 | import { DialogRef } from './dialog-ref';
4 | import { DialogService } from './dialog.service';
5 |
6 | @Directive({
7 | selector: '[dialogClose]',
8 | standalone: true,
9 | })
10 | export class DialogCloseDirective implements OnInit {
11 | private host: ElementRef = inject(ElementRef);
12 | private dialogService = inject(DialogService);
13 | ref: DialogRef = inject(DialogRef, { optional: true });
14 |
15 | @Input()
16 | dialogClose: any;
17 |
18 | ngOnInit() {
19 | this.ref = this.ref || this.getRefFromParent();
20 | }
21 |
22 | @HostListener('click')
23 | onClose() {
24 | this.ref.close(this.dialogClose);
25 | }
26 |
27 | private getRefFromParent() {
28 | let parent = this.host.nativeElement.parentElement;
29 |
30 | while (parent && parent.localName !== 'ngneat-dialog') {
31 | parent = parent.parentElement;
32 | }
33 |
34 | return parent ? this.dialogService.dialogs.find(({ id }) => id === parent.id) : null;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/libs/dialog/src/lib/dialog-ref.ts:
--------------------------------------------------------------------------------
1 | import { ComponentRef, TemplateRef } from '@angular/core';
2 | import { from, merge, Observable, of, Subject } from 'rxjs';
3 | import { defaultIfEmpty, filter, first } from 'rxjs/operators';
4 |
5 | import { DialogConfig, GlobalDialogConfig, JustProps } from './types';
6 | import { DragOffset } from './draggable.directive';
7 |
8 | type GuardFN = (result?: R) => Observable | Promise | boolean;
9 |
10 | export abstract class DialogRef<
11 | Data = any,
12 | Result = any,
13 | Ref extends ComponentRef | TemplateRef = ComponentRef | TemplateRef,
14 | > {
15 | ref: Ref;
16 | data: Data;
17 | id: string;
18 | backdropClick$: Observable;
19 | afterClosed$: Observable;
20 |
21 | abstract close(result?: Result): void;
22 | abstract beforeClose(guard: GuardFN): void;
23 | abstract resetDrag(offset?: DragOffset): void;
24 | abstract updateConfig(config: Partial): void;
25 | }
26 |
27 | type InternalDialogRefProps = Partial<
28 | Omit, 'id' | 'data'> & Pick
29 | >;
30 |
31 | export class InternalDialogRef extends DialogRef {
32 | config: DialogConfig & GlobalDialogConfig;
33 | backdropClick$: Subject;
34 | beforeCloseGuards: GuardFN[] = [];
35 | onClose: (result?: unknown) => void;
36 | onReset: (offset?: DragOffset) => void;
37 |
38 | constructor(props: InternalDialogRefProps = {}) {
39 | super();
40 | this.mutate(props);
41 | }
42 |
43 | close(result?: unknown): void {
44 | this.canClose(result)
45 | .pipe(filter(Boolean))
46 | .subscribe({ next: () => this.onClose(result) });
47 | }
48 |
49 | beforeClose(guard: GuardFN) {
50 | this.beforeCloseGuards.push(guard);
51 | }
52 |
53 | resetDrag(offset?: DragOffset) {
54 | this.onReset(offset);
55 | }
56 |
57 | canClose(result: unknown): Observable {
58 | const guards$ = this.beforeCloseGuards
59 | .map((guard) => guard(result))
60 | .filter((value) => value !== undefined && value !== true)
61 | .map((value) => {
62 | return typeof value === 'boolean' ? of(value) : from(value).pipe(filter((canClose) => !canClose));
63 | });
64 |
65 | return merge(...guards$).pipe(defaultIfEmpty(true), first());
66 | }
67 |
68 | mutate(props: InternalDialogRefProps) {
69 | Object.assign(this, props);
70 | this.data = this.config.data;
71 | this.id = this.config.id;
72 | }
73 |
74 | updateConfig(config: Partial) {
75 | this.mutate({
76 | config: {
77 | ...this.config,
78 | ...config,
79 | },
80 | });
81 | }
82 |
83 | asDialogRef(): DialogRef {
84 | return this;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/libs/dialog/src/lib/dialog.component.scss:
--------------------------------------------------------------------------------
1 | .ngneat-dialog-content {
2 | display: flex;
3 | flex-direction: column;
4 | overflow: hidden;
5 | position: relative;
6 |
7 | @keyframes dialog-open {
8 | 0% {
9 | transform: translateX(50px);
10 | }
11 |
12 | 100% {
13 | transform: none;
14 | }
15 | }
16 |
17 | animation: dialog-open 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
18 |
19 | border-radius: var(--dialog-content-border-radius, 4px);
20 | box-sizing: border-box;
21 |
22 | box-shadow: var(--dialog-content-box-shadow, 0px 11px 19px rgba(15, 20, 58, 0.14));
23 | background: var(--dialog-content-bg, #fff);
24 |
25 | width: auto;
26 | max-width: 100%;
27 |
28 | height: auto;
29 | max-height: 100%;
30 |
31 | &.ngneat-dialog-resizable {
32 | resize: both;
33 | }
34 | }
35 |
36 | .ngneat-dialog-backdrop {
37 | position: fixed;
38 | display: flex;
39 | align-items: center;
40 | justify-content: center;
41 | top: 0;
42 | left: 0;
43 | bottom: 0;
44 | right: 0;
45 | height: 100%;
46 | width: 100%;
47 | padding: 30px;
48 | z-index: var(--dialog-backdrop-z-index, 1050);
49 | background-color: transparent;
50 |
51 | &.ngneat-dialog-backdrop-visible {
52 | background: var(--dialog-backdrop-bg, rgba(0, 0, 0, 0.32));
53 | }
54 |
55 | animation: dialog-open-backdrop 0.3s;
56 |
57 | @keyframes dialog-open-backdrop {
58 | 0% {
59 | opacity: 0;
60 | }
61 |
62 | 100% {
63 | opacity: 1;
64 | }
65 | }
66 | }
67 |
68 | .ngneat-drag-marker {
69 | position: absolute;
70 | left: 0;
71 | top: 0;
72 | cursor: move;
73 | width: 100%;
74 | height: 10px;
75 | }
76 |
77 | .ngneat-close-dialog {
78 | display: flex;
79 | align-items: center;
80 | justify-content: center;
81 | position: absolute;
82 | cursor: pointer;
83 | top: var(--dialog-close-btn-top, 6px);
84 | right: var(--dialog-close-btn-right, 10px);
85 | width: var(--dialog-close-btn-size, 30px);
86 | height: var(--dialog-close-btn-size, 30px);
87 | color: var(--dialog-close-btn-color, #5f6368);
88 | transition: all 0.2s ease-in-out;
89 | border-radius: 50%;
90 |
91 | svg {
92 | width: var(--dialog-close-svg-size, 12px);
93 | height: var(--dialog-close-svg-size, 12px);
94 | }
95 |
96 | &:hover {
97 | color: var(--dialog-close-btn-color-hover, #5f6368);
98 | background-color: var(--dialog-close-btn-bg-hover, #eee);
99 | }
100 | }
101 |
102 | body {
103 | overflow: var(--dialog-overflow, hidden);
104 | }
105 |
--------------------------------------------------------------------------------
/libs/dialog/src/lib/dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule, DOCUMENT } from '@angular/common';
2 | import {
3 | Component,
4 | ElementRef,
5 | inject,
6 | isDevMode,
7 | OnDestroy,
8 | OnInit,
9 | ViewChild,
10 | ViewEncapsulation,
11 | } from '@angular/core';
12 | import { fromEvent, merge, Subject } from 'rxjs';
13 | import { filter, map, takeUntil } from 'rxjs/operators';
14 |
15 | import { InternalDialogRef } from './dialog-ref';
16 | import { DialogService } from './dialog.service';
17 | import { coerceCssPixelValue } from './dialog.utils';
18 | import { DialogDraggableDirective, DragOffset } from './draggable.directive';
19 | import { NODES_TO_INSERT } from './providers';
20 |
21 | @Component({
22 | selector: 'ngneat-dialog',
23 | standalone: true,
24 | imports: [DialogDraggableDirective, CommonModule],
25 | template: `
26 |
32 |
39 | @if (config.draggable) {
40 |
47 | } @if (config.closeButton) {
48 |
56 | }
57 |
58 |
59 | `,
60 | styleUrls: [`./dialog.component.scss`],
61 | encapsulation: ViewEncapsulation.None,
62 | })
63 | export class DialogComponent implements OnInit, OnDestroy {
64 | dialogRef = inject(InternalDialogRef);
65 | config = this.dialogRef.config;
66 |
67 | private size = this.config.sizes?.[this.config.size || 'md'];
68 | styles = {
69 | width: coerceCssPixelValue(this.config.width || this.size?.width),
70 | minWidth: coerceCssPixelValue(this.config.minWidth || this.size?.minWidth),
71 | maxWidth: coerceCssPixelValue(this.config.maxWidth || this.size?.maxWidth),
72 | height: coerceCssPixelValue(this.config.height || this.size?.height),
73 | minHeight: coerceCssPixelValue(this.config.minHeight || this.size?.minHeight),
74 | maxHeight: coerceCssPixelValue(this.config.maxHeight || this.size?.maxHeight),
75 | };
76 |
77 | @ViewChild('backdrop', { static: true })
78 | private backdrop: ElementRef;
79 |
80 | @ViewChild('dialog', { static: true })
81 | private dialogElement: ElementRef;
82 |
83 | @ViewChild(DialogDraggableDirective, { static: false })
84 | private draggable: DialogDraggableDirective;
85 |
86 | private destroy$ = new Subject();
87 |
88 | private nodes = inject(NODES_TO_INSERT);
89 |
90 | private document = inject(DOCUMENT);
91 | private host: HTMLElement = inject(ElementRef).nativeElement;
92 |
93 | private dialogService = inject(DialogService);
94 |
95 | constructor() {
96 | // Append nodes to dialog component, template or component could need
97 | // something from the dialog component
98 | // for example, if `[dialogClose]` is used into a directive,
99 | // DialogRef will be getted from DialogService instead of DI
100 | this.nodes.forEach((node) => this.host.appendChild(node));
101 |
102 | if (this.config.windowClass) {
103 | const classNames = this.config.windowClass.split(/\s/).filter((x) => x);
104 | classNames.forEach((name) => this.host.classList.add(name));
105 | }
106 |
107 | if (!this.config.id) {
108 | const id = `dialog-${crypto.randomUUID()}`;
109 | this.config.id = id;
110 | this.dialogRef.updateConfig({ id });
111 | if (isDevMode()) {
112 | console.warn(
113 | `[@ngneat/dialog]: Dialog id is not provided, generated id is ${id}, providing an id is recommended to prevent unexpected multiple behavior`,
114 | );
115 | }
116 | }
117 |
118 | if (this.config.overflow) {
119 | document.body.style.setProperty('--dialog-overflow', 'visible');
120 | }
121 |
122 | this.host.id = this.config.id;
123 | }
124 |
125 | ngOnInit() {
126 | const backdrop = this.config.backdrop ? this.backdrop.nativeElement : this.document.body;
127 | const dialogElement = this.dialogElement.nativeElement;
128 |
129 | const backdropClick$ = fromEvent(backdrop, 'click', { capture: true }).pipe(
130 | filter(({ target }) => !dialogElement.contains(target as Element)),
131 | );
132 |
133 | backdropClick$.pipe(takeUntil(this.destroy$)).subscribe(this.dialogRef.backdropClick$);
134 |
135 | // backwards compatibility with non-split option
136 | const closeConfig =
137 | typeof this.config.enableClose === 'boolean' || this.config.enableClose === 'onlyLastStrategy'
138 | ? {
139 | escape: this.config.enableClose,
140 | backdrop: this.config.enableClose,
141 | }
142 | : this.config.enableClose;
143 | merge(
144 | fromEvent(this.document.body, 'keyup').pipe(
145 | filter(({ key }) => key === 'Escape'),
146 | map(() => closeConfig.escape),
147 | ),
148 | backdropClick$.pipe(
149 | filter(() => this.document.getSelection()?.toString() === ''),
150 | map(() => closeConfig.backdrop),
151 | ),
152 | )
153 | .pipe(
154 | takeUntil(this.destroy$),
155 | filter((strategy) => {
156 | if (!strategy) return false;
157 | if (strategy === 'onlyLastStrategy') {
158 | return this.dialogService.isLastOpened(this.config.id);
159 | }
160 | return true;
161 | }),
162 | )
163 | .subscribe(() => this.closeDialog());
164 |
165 | // `dialogElement` is resolved at this point
166 | // And here is where dialog finally will be placed
167 | this.nodes.forEach((node) => dialogElement.appendChild(node));
168 |
169 | if (this.config.zIndexGetter) {
170 | const zIndex = this.config.zIndexGetter().toString();
171 | backdrop.style.setProperty('--dialog-backdrop-z-index', zIndex);
172 | }
173 | }
174 |
175 | reset(offset?: DragOffset): void {
176 | if (this.config.draggable) {
177 | this.draggable.reset(offset);
178 | }
179 | }
180 |
181 | closeDialog() {
182 | this.dialogRef.close();
183 | }
184 |
185 | ngOnDestroy() {
186 | this.destroy$.next();
187 | this.destroy$.complete();
188 |
189 | this.dialogRef = null;
190 | this.nodes = null;
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/libs/dialog/src/lib/dialog.service.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ApplicationRef,
3 | createComponent,
4 | ElementRef,
5 | EnvironmentInjector,
6 | inject,
7 | Injectable,
8 | Injector,
9 | reflectComponentType,
10 | TemplateRef,
11 | Type,
12 | ViewRef,
13 | } from '@angular/core';
14 | import { BehaviorSubject, Subject } from 'rxjs';
15 |
16 | import { DialogRef, InternalDialogRef } from './dialog-ref';
17 | import { DialogComponent } from './dialog.component';
18 | import { DragOffset } from './draggable.directive';
19 | import { DIALOG_DOCUMENT_REF, GLOBAL_DIALOG_CONFIG, NODES_TO_INSERT } from './providers';
20 | import { AttachOptions, DialogConfig, ExtractData, ExtractResult, GlobalDialogConfig } from './types';
21 |
22 | const OVERFLOW_HIDDEN_CLASS = 'ngneat-dialog-hidden';
23 |
24 | @Injectable({ providedIn: 'root' })
25 | export class DialogService {
26 | private appRef = inject(ApplicationRef);
27 | private injector = inject(EnvironmentInjector);
28 | private document = inject(DIALOG_DOCUMENT_REF);
29 | private globalConfig = inject(GLOBAL_DIALOG_CONFIG);
30 |
31 | // Replace with Map in next major version
32 | dialogs: DialogRef[] = [];
33 | // A Stream representing opening & closing dialogs
34 | private hasOpenDialogSub = new BehaviorSubject(false);
35 | hasOpenDialogs$ = this.hasOpenDialogSub.asObservable();
36 |
37 | hasOpenDialogs() {
38 | return this.dialogs.length > 0;
39 | }
40 |
41 | isOpen(id: string) {
42 | return this.dialogs.some((ref) => ref.id === id);
43 | }
44 |
45 | isLastOpened(idOrRef: string | DialogRef): boolean {
46 | const id = idOrRef instanceof DialogRef ? idOrRef.id : idOrRef;
47 |
48 | return this.dialogs.at(-1)?.id === id;
49 | }
50 |
51 | closeAll() {
52 | this.dialogs.forEach((dialog) => dialog.close());
53 | }
54 |
55 | open(template: TemplateRef, config?: Partial): DialogRef;
56 | open>(
57 | component: C,
58 | config?: Partial>>>,
59 | ): DialogRef>, ExtractResult>>;
60 | open(componentOrTemplate: any, config: Partial = {}): DialogRef {
61 | const mergedConfig = this.mergeConfig(config);
62 |
63 | if (isComponent(componentOrTemplate)) {
64 | mergedConfig.id ??= reflectComponentType(componentOrTemplate)?.selector;
65 | }
66 |
67 | if (this.isOpen(mergedConfig.id)) {
68 | return;
69 | }
70 |
71 | const dialogRef = new InternalDialogRef({
72 | config: mergedConfig,
73 | backdropClick$: new Subject(),
74 | });
75 |
76 | const attachOptions = isTemplate(componentOrTemplate)
77 | ? this.openTemplate(componentOrTemplate, dialogRef)
78 | : isComponent(componentOrTemplate)
79 | ? this.openComponent(componentOrTemplate, dialogRef)
80 | : throwMustBeAComponentOrATemplateRef(componentOrTemplate);
81 |
82 | if (this.isOpen(dialogRef.id)) {
83 | attachOptions.view.destroy();
84 | return;
85 | }
86 |
87 | mergedConfig.onOpen?.();
88 |
89 | this.dialogs.push(dialogRef);
90 | this.hasOpenDialogSub.next(true);
91 |
92 | if (this.dialogs.length === 1) {
93 | this.document.body.classList.add(OVERFLOW_HIDDEN_CLASS);
94 | }
95 |
96 | return this.attach(dialogRef, attachOptions);
97 | }
98 |
99 | private openTemplate(template: TemplateRef, dialogRef: InternalDialogRef) {
100 | const config = dialogRef.config;
101 | const context = {
102 | $implicit: dialogRef,
103 | config,
104 | };
105 |
106 | const view = config.vcr?.createEmbeddedView(template, context) || template.createEmbeddedView(context);
107 |
108 | return {
109 | ref: template,
110 | view,
111 | attachToApp: !config.vcr,
112 | };
113 | }
114 |
115 | private openComponent(Component: Type, dialogRef: InternalDialogRef) {
116 | const componentRef = createComponent(Component, {
117 | elementInjector: Injector.create({
118 | providers: [
119 | {
120 | provide: DialogRef,
121 | useValue: dialogRef,
122 | },
123 | ],
124 | parent: dialogRef.config.vcr?.injector || this.injector,
125 | }),
126 | environmentInjector: this.injector,
127 | });
128 |
129 | return {
130 | ref: componentRef,
131 | view: componentRef.hostView,
132 | attachToApp: true,
133 | };
134 | }
135 |
136 | private attach(dialogRef: InternalDialogRef, { ref, view, attachToApp }: AttachOptions): DialogRef {
137 | const dialog = this.createDialog(dialogRef, view);
138 |
139 | const container = getNativeElement(dialogRef.config.container);
140 |
141 | const hooks = {
142 | after: new Subject(),
143 | };
144 |
145 | const onClose = (result: unknown) => {
146 | this.globalConfig.onClose?.();
147 | this.dialogs = this.dialogs.filter(({ id }) => dialogRef.id !== id);
148 | this.hasOpenDialogSub.next(this.hasOpenDialogs());
149 |
150 | container.removeChild(dialog.location.nativeElement);
151 | this.appRef.detachView(dialog.hostView);
152 | this.appRef.detachView(view);
153 |
154 | dialog.destroy();
155 | view.destroy();
156 |
157 | dialogRef.backdropClick$.complete();
158 |
159 | dialogRef.mutate({
160 | ref: null,
161 | onClose: null,
162 | afterClosed$: null,
163 | backdropClick$: null,
164 | beforeCloseGuards: null,
165 | onReset: null,
166 | });
167 |
168 | hooks.after.next(result);
169 | hooks.after.complete();
170 | if (!this.hasOpenDialogs()) {
171 | this.document.body.classList.remove(OVERFLOW_HIDDEN_CLASS);
172 | }
173 | };
174 |
175 | const onReset = (offset?: DragOffset) => {
176 | dialog.instance.reset(offset);
177 | };
178 |
179 | dialogRef.mutate({
180 | ref,
181 | onClose,
182 | afterClosed$: hooks.after.asObservable(),
183 | onReset,
184 | });
185 |
186 | container.appendChild(dialog.location.nativeElement);
187 | this.appRef.attachView(dialog.hostView);
188 |
189 | if (attachToApp) {
190 | this.appRef.attachView(view);
191 | }
192 |
193 | return dialogRef.asDialogRef();
194 | }
195 |
196 | private createDialog(dialogRef: InternalDialogRef, view: ViewRef) {
197 | return createComponent(DialogComponent, {
198 | elementInjector: Injector.create({
199 | providers: [
200 | {
201 | provide: InternalDialogRef,
202 | useValue: dialogRef,
203 | },
204 | {
205 | provide: NODES_TO_INSERT,
206 | useValue: (view as any).rootNodes,
207 | },
208 | ],
209 | parent: this.injector,
210 | }),
211 | environmentInjector: this.injector,
212 | });
213 | }
214 |
215 | private mergeConfig(inlineConfig: Partial): DialogConfig & GlobalDialogConfig {
216 | return {
217 | ...this.globalConfig,
218 | ...inlineConfig,
219 | sizes: this.globalConfig?.sizes,
220 | } as DialogConfig & GlobalDialogConfig;
221 | }
222 | }
223 |
224 | function throwMustBeAComponentOrATemplateRef(value: unknown): never {
225 | throw new TypeError(`Dialog must receive a Component or a TemplateRef, but this has been passed instead: ${value}`);
226 | }
227 |
228 | function getNativeElement(element: Element | ElementRef): Element {
229 | return element instanceof ElementRef ? element.nativeElement : element;
230 | }
231 |
232 | function isTemplate(tplOrComp: any): tplOrComp is TemplateRef {
233 | return tplOrComp instanceof TemplateRef;
234 | }
235 |
236 | function isComponent(tplOrComp: any): tplOrComp is Type {
237 | return !isTemplate(tplOrComp) && typeof tplOrComp === 'function';
238 | }
239 |
--------------------------------------------------------------------------------
/libs/dialog/src/lib/dialog.utils.ts:
--------------------------------------------------------------------------------
1 | function isNil(value: unknown): value is undefined | null {
2 | return value === undefined || value === null;
3 | }
4 |
5 | function isString(value: unknown): value is string {
6 | return typeof value === 'string';
7 | }
8 |
9 | export function coerceCssPixelValue(value: any): string {
10 | if (isNil(value)) {
11 | return '';
12 | }
13 |
14 | return isString(value) ? value : `${value}px`;
15 | }
16 |
--------------------------------------------------------------------------------
/libs/dialog/src/lib/draggable.directive.ts:
--------------------------------------------------------------------------------
1 | import { AfterViewInit, Directive, ElementRef, inject, Input, NgZone, OnChanges, OnDestroy } from '@angular/core';
2 | import { fromEvent, Subject } from 'rxjs';
3 | import { filter, map, switchMap, takeUntil } from 'rxjs/operators';
4 |
5 | import { DragConstraint } from './types';
6 |
7 | export type DragOffset = {
8 | x?: number;
9 | y?: number;
10 | };
11 |
12 | @Directive({
13 | selector: '[dialogDraggable]',
14 | standalone: true,
15 | })
16 | export class DialogDraggableDirective implements AfterViewInit, OnChanges, OnDestroy {
17 | @Input()
18 | dialogDragHandle: string | Element;
19 | @Input()
20 | dialogDragTarget: string | Element;
21 | @Input()
22 | dialogDragEnabled = false;
23 | @Input()
24 | set dialogDragOffset(offset: DragOffset) {
25 | this.reset(offset);
26 | }
27 | @Input()
28 | dragConstraint: DragConstraint;
29 |
30 | private host = inject(ElementRef);
31 | private zone = inject(NgZone);
32 |
33 | /** Element to be dragged */
34 | private target: HTMLElement;
35 | /** Drag handle */
36 | private handle: HTMLElement;
37 | private delta = { x: 0, y: 0 };
38 | private offset = { x: 0, y: 0 };
39 | private enabled = true;
40 | private destroy$ = new Subject();
41 |
42 | public ngAfterViewInit(): void {
43 | if (!this.enabled) {
44 | return;
45 | }
46 |
47 | this.init();
48 | }
49 |
50 | public ngOnChanges() {
51 | if (!this.enabled && this.dialogDragEnabled && this.dialogDragTarget) {
52 | this.enabled = true;
53 | /** determine if the component has been init by the handle variable */
54 | if (this.handle) {
55 | this.handle.style.setProperty('cursor', 'move');
56 | } else if (this.enabled) {
57 | this.init();
58 | }
59 | }
60 |
61 | if (!this.dialogDragEnabled) {
62 | this.enabled = false;
63 | if (this.handle) {
64 | this.handle.style.setProperty('cursor', '');
65 | }
66 | }
67 | }
68 |
69 | public ngOnDestroy(): void {
70 | this.destroy$.next();
71 | }
72 |
73 | public reset(offset?: DragOffset): void {
74 | const defaultValues = { x: 0, y: 0 };
75 | this.offset = { ...defaultValues, ...offset };
76 | this.delta = { ...defaultValues };
77 | this.translate();
78 | }
79 |
80 | private setupEvents() {
81 | this.zone.runOutsideAngular(() => {
82 | const mousedown$ = fromEvent(this.handle, 'mousedown');
83 | const mousemove$ = fromEvent(document, 'mousemove');
84 | const mouseup$ = fromEvent(document, 'mouseup');
85 |
86 | const mousedrag$ = mousedown$.pipe(
87 | filter(() => this.enabled),
88 | map((event) => ({
89 | startX: event.clientX,
90 | startY: event.clientY,
91 | })),
92 | switchMap(({ startX, startY }) =>
93 | mousemove$.pipe(
94 | map((event) => {
95 | event.preventDefault();
96 | this.delta = {
97 | x: event.clientX - startX,
98 | y: event.clientY - startY,
99 | };
100 | if (this.dragConstraint === 'constrain') {
101 | this.checkConstraint();
102 | }
103 | }),
104 | takeUntil(mouseup$),
105 | ),
106 | ),
107 | takeUntil(this.destroy$),
108 | );
109 |
110 | mousedrag$.subscribe(() => {
111 | if (this.delta.x === 0 && this.delta.y === 0) {
112 | return;
113 | }
114 |
115 | this.translate();
116 | });
117 |
118 | mouseup$
119 | .pipe(
120 | filter(() => this.enabled),
121 | /** Only emit change if the element has moved */
122 | filter(() => this.delta.x !== 0 || this.delta.y !== 0),
123 | takeUntil(this.destroy$),
124 | )
125 | .subscribe(() => {
126 | if (this.dragConstraint === 'bounce') {
127 | this.checkConstraint();
128 | this.translate();
129 | }
130 | this.offset.x += this.delta.x;
131 | this.offset.y += this.delta.y;
132 | this.delta = { x: 0, y: 0 };
133 | });
134 | });
135 | }
136 |
137 | private translate() {
138 | if (this.target) {
139 | this.zone.runOutsideAngular(() => {
140 | requestAnimationFrame(() => {
141 | const transform = `translate(${this.translateX}px, ${this.translateY}px)`;
142 | this.target.style.setProperty('transform', transform);
143 | });
144 | });
145 | }
146 | }
147 |
148 | private get translateX(): number {
149 | return this.offset.x + this.delta.x;
150 | }
151 |
152 | private get translateY(): number {
153 | return this.offset.y + this.delta.y;
154 | }
155 |
156 | /**
157 | * Init the directive
158 | */
159 | private init() {
160 | if (!this.dialogDragTarget) {
161 | throw new Error('You need to specify the drag target');
162 | }
163 |
164 | this.handle =
165 | this.dialogDragHandle instanceof Element
166 | ? this.dialogDragHandle
167 | : typeof this.dialogDragHandle === 'string' && this.dialogDragHandle
168 | ? document.querySelector(this.dialogDragHandle as string)
169 | : this.host.nativeElement;
170 |
171 | /** add the move cursor */
172 | if (this.handle && this.enabled) {
173 | this.handle.style.setProperty('cursor', 'move');
174 | }
175 |
176 | this.target =
177 | this.dialogDragTarget instanceof HTMLElement
178 | ? this.dialogDragTarget
179 | : document.querySelector(this.dialogDragTarget as string);
180 |
181 | this.setupEvents();
182 |
183 | this.translate();
184 | }
185 |
186 | private checkConstraint(): void {
187 | const { width, height } = this.target.getBoundingClientRect();
188 | const { innerWidth, innerHeight } = window;
189 |
190 | const verticalDistance = this.translateY > 0 ? this.translateY + height / 2 : this.translateY - height / 2;
191 | const maxVerticalDistance = innerHeight / 2;
192 | const horizontalDistance = this.translateX > 0 ? this.translateX + width / 2 : this.translateX - width / 2;
193 | const maxHorizontalDistance = innerWidth / 2;
194 |
195 | // Check if modal crosses the top, bottom, left and right window border respectively
196 | if (-maxVerticalDistance > verticalDistance) {
197 | this.delta.y = -maxVerticalDistance + height / 2 - this.offset.y;
198 | }
199 | if (maxVerticalDistance < verticalDistance) {
200 | this.delta.y = maxVerticalDistance - height / 2 - this.offset.y;
201 | }
202 | if (-maxHorizontalDistance > horizontalDistance) {
203 | this.delta.x = -maxHorizontalDistance + width / 2 - this.offset.x;
204 | }
205 | if (maxHorizontalDistance < horizontalDistance) {
206 | this.delta.x = maxHorizontalDistance - width / 2 - this.offset.x;
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/libs/dialog/src/lib/providers.ts:
--------------------------------------------------------------------------------
1 | import { DOCUMENT } from '@angular/common';
2 | import { inject, InjectionToken, makeEnvironmentProviders } from '@angular/core';
3 |
4 | import { DialogConfig, GlobalDialogConfig } from './types';
5 |
6 | export const DIALOG_DOCUMENT_REF = new InjectionToken(
7 | 'A reference to the document. Useful for iframes that want appends to parent window',
8 | {
9 | providedIn: 'root',
10 | factory() {
11 | return inject(DOCUMENT);
12 | },
13 | },
14 | );
15 |
16 | export function defaultGlobalConfig(): Partial {
17 | return {
18 | id: undefined,
19 | container: inject(DIALOG_DOCUMENT_REF).body,
20 | backdrop: true,
21 | closeButton: true,
22 | enableClose: {
23 | backdrop: true,
24 | escape: true,
25 | },
26 | draggable: false,
27 | dragConstraint: 'none',
28 | resizable: false,
29 | size: 'md',
30 | windowClass: undefined,
31 | width: undefined,
32 | minWidth: undefined,
33 | maxWidth: undefined,
34 | height: undefined,
35 | minHeight: undefined,
36 | maxHeight: undefined,
37 | data: undefined,
38 | vcr: undefined,
39 | sizes: {
40 | sm: {
41 | height: 'auto',
42 | width: '400px',
43 | },
44 | md: {
45 | height: 'auto',
46 | width: '560px',
47 | },
48 | lg: {
49 | height: 'auto',
50 | width: '800px',
51 | },
52 | fullScreen: {
53 | height: '100%',
54 | width: '100%',
55 | },
56 | },
57 | onClose: undefined,
58 | onOpen: undefined,
59 | };
60 | }
61 |
62 | export const GLOBAL_DIALOG_CONFIG = new InjectionToken>('Global dialog config token', {
63 | providedIn: 'root',
64 | factory() {
65 | return defaultGlobalConfig();
66 | },
67 | });
68 |
69 | export const NODES_TO_INSERT = new InjectionToken('Nodes inserted into the dialog');
70 |
71 | export function provideDialogConfig(config: Partial) {
72 | return makeEnvironmentProviders([
73 | {
74 | provide: GLOBAL_DIALOG_CONFIG,
75 | useFactory() {
76 | const defaultConfig = defaultGlobalConfig();
77 | return {
78 | ...defaultConfig,
79 | ...config,
80 | sizes: {
81 | ...defaultConfig.sizes,
82 | ...config.sizes,
83 | },
84 | };
85 | },
86 | },
87 | ]);
88 | }
89 |
90 | export function provideDialogDocRef(doc: Document) {
91 | return makeEnvironmentProviders([
92 | {
93 | provide: DIALOG_DOCUMENT_REF,
94 | useValue: doc,
95 | },
96 | ]);
97 | }
98 |
--------------------------------------------------------------------------------
/libs/dialog/src/lib/specs/dialog-close.directive.spec.ts:
--------------------------------------------------------------------------------
1 | import { NO_ERRORS_SCHEMA } from '@angular/core';
2 | import { createDirectiveFactory } from '@ngneat/spectator';
3 |
4 | import { DialogCloseDirective } from '../dialog-close.directive';
5 | import { DialogService } from '../dialog.service';
6 | import { DialogRef } from '../dialog-ref';
7 |
8 | describe('DialogClose', () => {
9 | const createDirective = createDirectiveFactory({
10 | directive: DialogCloseDirective,
11 | schemas: [NO_ERRORS_SCHEMA],
12 | });
13 |
14 | it('should get dialog-ref getting id from parent a searching in dialog-service', () => {
15 | const dialogRefFromParent = { id: 'from-parent' };
16 |
17 | const spectator = createDirective(
18 | ' ',
19 | {
20 | providers: [
21 | {
22 | provide: DialogService,
23 | useValue: {
24 | dialogs: [dialogRefFromParent],
25 | },
26 | },
27 | ],
28 | },
29 | );
30 |
31 | expect(spectator.directive.ref).toBe(dialogRefFromParent as any);
32 | });
33 |
34 | it('should get dialog-ref from injector', () => {
35 | const dialogRefFromParent = { id: 'from-parent' };
36 | const dialogRefFromInjector = { id: 'from-injector' };
37 |
38 | const spectator = createDirective(' ', {
39 | providers: [
40 | {
41 | provide: DialogRef,
42 | useValue: dialogRefFromInjector,
43 | },
44 | {
45 | provide: DialogService,
46 | useValue: {
47 | dialogs: [dialogRefFromParent],
48 | },
49 | },
50 | ],
51 | });
52 |
53 | expect(spectator.directive.ref).toBe(dialogRefFromInjector as any);
54 | });
55 |
56 | it('on close should call dialog-ref close method, passing result', () => {
57 | const dialogRefFromInjector: Partial = {
58 | id: 'from-injector',
59 | close: jasmine.createSpy(),
60 | };
61 |
62 | const spectator = createDirective(` `, {
63 | providers: [
64 | {
65 | provide: DialogRef,
66 | useValue: dialogRefFromInjector,
67 | },
68 | {
69 | provide: DialogService,
70 | useValue: {
71 | dialogs: [],
72 | },
73 | },
74 | ],
75 | });
76 |
77 | spectator.click(spectator.query('button'));
78 |
79 | spectator.detectChanges();
80 |
81 | expect(dialogRefFromInjector.close).toHaveBeenCalledWith('something');
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/libs/dialog/src/lib/specs/dialog.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { byText, createComponentFactory, Spectator, SpyObject } from '@ngneat/spectator';
2 | import { Subject } from 'rxjs';
3 |
4 | import { InternalDialogRef } from '../dialog-ref';
5 | import { DialogComponent } from '../dialog.component';
6 | import { DialogService } from '../dialog.service';
7 | import { DialogDraggableDirective } from '../draggable.directive';
8 | import { NODES_TO_INSERT } from '../providers';
9 | import { CloseStrategy, DialogConfig, GlobalDialogConfig } from '../types';
10 |
11 | describe('DialogComponent', () => {
12 | const defaultConfig: Partial = {
13 | id: 'test',
14 | container: document.body,
15 | backdrop: true,
16 | enableClose: true,
17 | draggable: false,
18 | resizable: false,
19 | size: 'md',
20 | windowClass: undefined,
21 | sizes: undefined,
22 | width: undefined,
23 | height: undefined,
24 | data: undefined,
25 | vcr: undefined,
26 | };
27 |
28 | let config = defaultConfig;
29 | function setConfig(inline: Partial = {}) {
30 | config = { ...defaultConfig, ...inline };
31 | }
32 |
33 | let spectator: Spectator;
34 |
35 | const createComponent = createComponentFactory({
36 | component: DialogComponent,
37 | imports: [DialogDraggableDirective],
38 | providers: [
39 | {
40 | provide: InternalDialogRef,
41 | useFactory: () => ({
42 | close: jasmine.createSpy(),
43 | backdropClick$: new Subject(),
44 | get config() {
45 | return config;
46 | },
47 | }),
48 | },
49 | {
50 | provide: NODES_TO_INSERT,
51 | useValue: [document.createTextNode('nodes '), document.createTextNode('inserted')],
52 | },
53 | ],
54 | });
55 |
56 | beforeEach(() => {
57 | setConfig();
58 | });
59 |
60 | afterEach(() => {
61 | const containerEls = document.querySelectorAll('.ngneat-dialog-content');
62 | const backdropEls = document.querySelectorAll('.ngneat-dialog-backdrop');
63 |
64 | [...Array.from(containerEls), ...Array.from(backdropEls)].filter(Boolean).forEach((el) => el.remove());
65 | });
66 |
67 | it('should create', () => {
68 | spectator = createComponent();
69 | expect(spectator.component).toBeTruthy();
70 | });
71 |
72 | it('should set id in its host', () => {
73 | setConfig({ id: 'test' });
74 | spectator = createComponent();
75 |
76 | spectator.detectChanges();
77 |
78 | expect(spectator.element.id).toBe('test');
79 | });
80 |
81 | it('should place nodes into dialog-content', () => {
82 | spectator = createComponent();
83 |
84 | expect(spectator.query(byText('nodes inserted'))).toBeTruthy();
85 | });
86 |
87 | describe('when backdrop is enabled', () => {
88 | beforeEach(() => {
89 | setConfig({ backdrop: true });
90 | spectator = createComponent();
91 | });
92 |
93 | it('should create backdrop div, and set its class', () => {
94 | expect(spectator.query('.ngneat-dialog-backdrop')).toBeTruthy();
95 | });
96 |
97 | it('should not close when text is selected', () => {
98 | const { close } = spectator.inject(InternalDialogRef);
99 | spyOn(document, 'getSelection').and.returnValue({ toString: () => 'selected text' } as Selection);
100 |
101 | spectator.dispatchMouseEvent('.ngneat-dialog-backdrop', 'click');
102 | expect(close).not.toHaveBeenCalled();
103 | });
104 |
105 | it('backdropClick$ should point to element', () => {
106 | let backdropClicked = false;
107 | spyOn(document, 'getSelection').and.returnValue({ toString: () => '' } as Selection);
108 | spectator.inject(InternalDialogRef).backdropClick$.subscribe({
109 | next: () => (backdropClicked = true),
110 | });
111 |
112 | spectator.dispatchMouseEvent('.ngneat-dialog-backdrop', 'click');
113 |
114 | expect(backdropClicked).toBeTrue();
115 | });
116 | });
117 |
118 | describe('when backdrop is disabled', () => {
119 | beforeEach(() => {
120 | setConfig({ backdrop: false });
121 | spectator = createComponent();
122 | });
123 |
124 | it('should create backdrop div, and set its class', () => {
125 | expect(spectator.query('.ngneat-dialog-backdrop')).toBeHidden();
126 | });
127 |
128 | it('backdropClick$ should point to body', () => {
129 | let backdropClicked = false;
130 | spectator.inject(InternalDialogRef).backdropClick$.subscribe({
131 | next: () => (backdropClicked = true),
132 | });
133 |
134 | spectator.dispatchMouseEvent(document.body, 'click');
135 |
136 | expect(backdropClicked).toBeTrue();
137 | });
138 | });
139 |
140 | describe('enableClose', () => {
141 | const closeStrategies: CloseStrategy[] = [true, false, 'onlyLastStrategy'];
142 | const dialogPositions = ['only', 'last', 'not-last'];
143 | const cases: {
144 | _value: DialogConfig['enableClose'];
145 | _position: (typeof dialogPositions)[keyof typeof dialogPositions];
146 | backdrop: CloseStrategy;
147 | escape: CloseStrategy;
148 | }[] = [
149 | ...closeStrategies.map((_value) => ({
150 | _value,
151 | escape: _value,
152 | backdrop: _value,
153 | })),
154 | ...closeStrategies.flatMap((escape) =>
155 | closeStrategies.map((backdrop) => ({
156 | _value: { escape, backdrop },
157 | escape,
158 | backdrop,
159 | })),
160 | ),
161 | ].flatMap((context) =>
162 | dialogPositions.map((pos) => ({
163 | _value: context._value,
164 | _position: pos,
165 | backdrop: context.backdrop === 'onlyLastStrategy' ? pos !== 'not-last' : context.backdrop,
166 | escape: context.escape === 'onlyLastStrategy' ? pos !== 'not-last' : context.escape,
167 | })),
168 | );
169 | cases.forEach(({ _value, _position, backdrop, escape }) => {
170 | describe(`set to ${JSON.stringify(_value)}`, () => {
171 | describe(`as the ${_position} dialog`, () => {
172 | let dialogService: SpyObject;
173 | beforeEach(() => {
174 | setConfig({ enableClose: _value, id: 'close-last-open-dialog' });
175 | spectator = createComponent();
176 | dialogService = spectator.inject(DialogService);
177 | dialogService.dialogs.push(Object.assign(spectator.component.dialogRef, { id: 'close-last-open-dialog' }));
178 | if (_position === 'last') dialogService.dialogs.splice(0, 0, { id: 'first-dialog' } as InternalDialogRef);
179 | if (_position === 'not-last') dialogService.dialogs.push({ id: 'last-dialog' } as InternalDialogRef);
180 | });
181 | if (backdrop !== false)
182 | it('should call close on backdrop click', () => {
183 | const { close } = spectator.inject(InternalDialogRef);
184 | spectator.dispatchMouseEvent('.ngneat-dialog-content', 'click');
185 | expect(close).not.toHaveBeenCalled();
186 | spectator.dispatchMouseEvent(document.body, 'click');
187 | expect(close).not.toHaveBeenCalled();
188 | spectator.dispatchMouseEvent('.ngneat-dialog-backdrop', 'click');
189 | expect(close).toHaveBeenCalled();
190 | });
191 | else
192 | it('should not call close on backdrop click', () => {
193 | const { close } = spectator.inject(InternalDialogRef);
194 | spectator.dispatchMouseEvent('.ngneat-dialog-content', 'click');
195 | expect(close).not.toHaveBeenCalled();
196 | spectator.dispatchMouseEvent(document.body, 'click');
197 | expect(close).not.toHaveBeenCalled();
198 | spectator.dispatchMouseEvent('.ngneat-dialog-backdrop', 'click');
199 | expect(close).not.toHaveBeenCalled();
200 | });
201 | if (escape !== false)
202 | it('should call close on escape', () => {
203 | const { close } = spectator.inject(InternalDialogRef);
204 | spectator.dispatchKeyboardEvent(document.body, 'keyup', 'Enter');
205 | expect(close).not.toHaveBeenCalled();
206 | spectator.dispatchKeyboardEvent(document.body, 'keyup', 'Escape');
207 | expect(close).toHaveBeenCalled();
208 | });
209 | else
210 | it('should not call close on escape', () => {
211 | const { close } = spectator.inject(InternalDialogRef);
212 | spectator.dispatchKeyboardEvent(document.body, 'keyup', 'Escape');
213 | expect(close).not.toHaveBeenCalled();
214 | });
215 | });
216 | });
217 | });
218 | });
219 |
220 | describe('when draggable is enabled', () => {
221 | beforeEach(() => {
222 | setConfig({ draggable: true });
223 | spectator = createComponent();
224 | });
225 |
226 | it('should show draggable marker and instance draggable directive', () => {
227 | expect(spectator.query('.ngneat-drag-marker')).toBeTruthy();
228 | expect(spectator.query(DialogDraggableDirective)).toBeTruthy();
229 | });
230 | });
231 |
232 | describe('when draggable is disabled', () => {
233 | beforeEach(() => {
234 | setConfig({ draggable: false });
235 | spectator = createComponent();
236 | });
237 |
238 | it('should not show draggable marker and not instance draggable directive', () => {
239 | expect(spectator.query('.ngneat-drag-marker')).toBeFalsy();
240 | expect(spectator.query(DialogDraggableDirective)).toBeFalsy();
241 | });
242 | });
243 |
244 | it('when resizable is enabled should set its class', () => {
245 | setConfig({ resizable: true });
246 | spectator = createComponent();
247 |
248 | expect(spectator.query('.ngneat-dialog-resizable')).toBeTruthy();
249 | });
250 |
251 | it('should set windowClass at host element', () => {
252 | setConfig({ windowClass: 'this-is-a-test-class' });
253 | spectator = createComponent();
254 |
255 | const host = spectator.query('.this-is-a-test-class', { root: true });
256 |
257 | expect(host).toBeTruthy();
258 | expect(host).toBe(spectator.fixture.nativeElement);
259 | });
260 |
261 | it('should set multiple classes from windowClass at host element', () => {
262 | setConfig({ windowClass: ' test-class-1 test-class-2 ' });
263 | spectator = createComponent();
264 |
265 | const host = spectator.query('.test-class-1.test-class-2', { root: true });
266 |
267 | expect(host).toBeTruthy();
268 | expect(host).toBe(spectator.fixture.nativeElement);
269 | });
270 |
271 | it('should add a role attribute to the dialog', () => {
272 | spectator = createComponent();
273 |
274 | expect(spectator.query('[role=dialog]')).toBeTruthy();
275 | });
276 | });
277 |
--------------------------------------------------------------------------------
/libs/dialog/src/lib/specs/dialog.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationRef, Component, TemplateRef } from '@angular/core';
2 | import { fakeAsync, tick } from '@angular/core/testing';
3 | import { createServiceFactory, SpectatorService } from '@ngneat/spectator';
4 | import { mapTo, timer } from 'rxjs';
5 |
6 | import { InternalDialogRef } from '../dialog-ref';
7 | import { DialogService } from '../dialog.service';
8 | import { DIALOG_DOCUMENT_REF, GLOBAL_DIALOG_CONFIG, provideDialogConfig } from '../providers';
9 |
10 | class FakeTemplateRef extends TemplateRef {
11 | elementRef = null;
12 | view = {
13 | rootNodes: [document.createTextNode('template'), document.createTextNode(' nodes')],
14 | destroy: jasmine.createSpy(),
15 | };
16 |
17 | createEmbeddedView = jasmine.createSpy().and.returnValue(this.view);
18 | }
19 |
20 | describe('DialogService', () => {
21 | let spectator: SpectatorService;
22 | let service: DialogService;
23 | let fakeAppRef: ApplicationRef;
24 | let fakeDocument: {
25 | body: {
26 | appendChild: jasmine.Spy;
27 | removeChild: jasmine.Spy;
28 | classList: { add: jasmine.Spy; remove: jasmine.Spy };
29 | };
30 | };
31 |
32 | const createService = createServiceFactory({
33 | service: DialogService,
34 | mocks: [ApplicationRef],
35 | providers: [
36 | provideDialogConfig({
37 | sizes: {
38 | sm: { width: '20px' },
39 | md: { width: '20px' },
40 | lg: { width: '20px' },
41 | fullScreen: { width: '20px' },
42 | },
43 | }),
44 | {
45 | provide: DIALOG_DOCUMENT_REF,
46 | useFactory: () => ({
47 | body: {
48 | appendChild: jasmine.createSpy(),
49 | removeChild: jasmine.createSpy(),
50 | classList: {
51 | add: jasmine.createSpy(),
52 | remove: jasmine.createSpy(),
53 | },
54 | },
55 | }),
56 | },
57 | ],
58 | });
59 |
60 | beforeEach(() => {
61 | spectator = createService();
62 | service = spectator.service;
63 | fakeAppRef = spectator.inject(ApplicationRef);
64 | fakeDocument = spectator.inject(DIALOG_DOCUMENT_REF);
65 | });
66 |
67 | it('should create it', () => {
68 | expect(service).toBeTruthy();
69 | });
70 |
71 | it('should overwrite sizes', () => {
72 | service.open(new FakeTemplateRef());
73 |
74 | expect(spectator.inject(GLOBAL_DIALOG_CONFIG).sizes).toEqual(
75 | jasmine.objectContaining({
76 | sm: { width: '20px' },
77 | md: { width: '20px' },
78 | lg: { width: '20px' },
79 | fullScreen: { width: '20px' },
80 | }),
81 | );
82 | });
83 |
84 | it('should generate valid dialog id', () => {
85 | const dialog = service.open(new FakeTemplateRef());
86 | const idValidityRegex = /^[A-Za-z]+[\w-:.]*$/;
87 |
88 | expect(idValidityRegex.test(dialog.id)).toBe(true);
89 | });
90 |
91 | it('should skip opening if two dialogs has the same id', () => {
92 | const onOpenSpy = jasmine.createSpy();
93 | const open = () => service.open(new FakeTemplateRef(), { id: 'same', onOpen: onOpenSpy });
94 | open();
95 | open();
96 | expect(onOpenSpy).toHaveBeenCalledTimes(1);
97 | });
98 |
99 | describe('using a template', () => {
100 | it('should open it', () => expect(service.open(new FakeTemplateRef())).toBeTruthy());
101 |
102 | it('should add dialog to dialogs', () => {
103 | const dialog = service.open(new FakeTemplateRef());
104 |
105 | expect(service.dialogs.length).toBe(1);
106 | expect(service.dialogs).toContain(dialog);
107 | });
108 |
109 | it('should instanciate template', () => {
110 | const fakeTemplate = new FakeTemplateRef();
111 | const dialog = service.open(fakeTemplate, {
112 | data: 'test',
113 | windowClass: 'custom-template',
114 | });
115 |
116 | expect(dialog.ref).toBe(fakeTemplate);
117 | expect(fakeTemplate.createEmbeddedView).toHaveBeenCalledTimes(1);
118 | expect(fakeTemplate.createEmbeddedView).toHaveBeenCalledWith({
119 | $implicit: dialog,
120 | config: jasmine.objectContaining({ windowClass: 'custom-template' }),
121 | });
122 | });
123 |
124 | it('should append dialog element into container', () => {
125 | service.open(new FakeTemplateRef());
126 |
127 | expect(fakeDocument.body.appendChild).toHaveBeenCalledTimes(1);
128 | });
129 |
130 | it('should use vcr to instanciate template', () => {
131 | const template = new FakeTemplateRef();
132 | const otherVCR = new FakeTemplateRef();
133 |
134 | const dialog = service.open(template, {
135 | vcr: otherVCR as any,
136 | data: 'test',
137 | windowClass: 'custom-template',
138 | });
139 |
140 | expect(template.createEmbeddedView).not.toHaveBeenCalled();
141 | expect(otherVCR.createEmbeddedView).toHaveBeenCalledWith(template, {
142 | $implicit: dialog,
143 | config: jasmine.objectContaining({ windowClass: 'custom-template' }),
144 | });
145 | });
146 | });
147 |
148 | describe('when nested', () => {
149 | it('should open both', () => {
150 | expect(service.open(new FakeTemplateRef())).toBeTruthy();
151 | expect(service.open(new FakeTemplateRef())).toBeTruthy();
152 | });
153 |
154 | it('should add both dialogs to dialogs', () => {
155 | const dialog1 = service.open(new FakeTemplateRef());
156 | const dialog2 = service.open(new FakeTemplateRef());
157 |
158 | expect(service.dialogs.length).toBe(2);
159 | expect(service.dialogs).toContain(dialog1);
160 | expect(service.dialogs).toContain(dialog2);
161 | });
162 |
163 | it('should add OVERFLOW_HIDDEN_CLASS to body only once', () => {
164 | service.open(new FakeTemplateRef());
165 | service.open(new FakeTemplateRef());
166 |
167 | expect(fakeDocument.body.classList.add).toHaveBeenCalledTimes(1);
168 | });
169 |
170 | it('should not remove OVERFLOW_HIDDEN_CLASS from when close one', () => {
171 | const dialog1 = service.open(new FakeTemplateRef());
172 | service.open(new FakeTemplateRef());
173 |
174 | dialog1.close();
175 | expect(fakeDocument.body.classList.remove).toHaveBeenCalledTimes(0);
176 | });
177 |
178 | it('should remove OVERFLOW_HIDDEN_CLASS from when close all', () => {
179 | const dialog1 = service.open(new FakeTemplateRef());
180 | const dialog2 = service.open(new FakeTemplateRef());
181 |
182 | dialog1.close();
183 | dialog2.close();
184 | expect(fakeDocument.body.classList.remove).toHaveBeenCalledTimes(1);
185 | });
186 | });
187 |
188 | describe('using a component', () => {
189 | @Component({ selector: '', template: '' })
190 | class FakeComponent {}
191 | it('should open it', () => expect(service.open(FakeComponent)).toBeTruthy());
192 |
193 | it('should add dialog to dialogs', () => {
194 | const dialog = service.open(FakeComponent);
195 | expect(service.dialogs.length).toBe(1);
196 | expect(service.dialogs).toContain(dialog);
197 | });
198 |
199 | it('should append dialog element into container', () => {
200 | service.open(FakeComponent);
201 | expect(fakeDocument.body.appendChild).toHaveBeenCalledTimes(1);
202 | });
203 | it('should attach view to ApplicationRef', () => {
204 | service.open(FakeComponent);
205 | expect(fakeAppRef.attachView).toHaveBeenCalledTimes(2);
206 | });
207 | });
208 |
209 | describe('on close', () => {
210 | let dialog: InternalDialogRef;
211 | let fakeTemplate: FakeTemplateRef;
212 |
213 | beforeEach(() => {
214 | fakeTemplate = new FakeTemplateRef();
215 | dialog = service.open(fakeTemplate) as InternalDialogRef;
216 | });
217 |
218 | describe('using beforeClose', () => {
219 | let dialogHasBeenClosed: boolean;
220 |
221 | beforeEach(() => {
222 | dialogHasBeenClosed = false;
223 | dialog.afterClosed$.subscribe({ next: () => (dialogHasBeenClosed = true) });
224 | });
225 |
226 | it('should close if there are no guards', () => {
227 | expect(dialog.beforeCloseGuards).toEqual([]);
228 |
229 | dialog.close();
230 |
231 | expect(dialogHasBeenClosed).toBeTrue();
232 | });
233 |
234 | it('should pass result to guard', () => {
235 | dialog.beforeClose((result) => {
236 | expect(result).toBe('test');
237 |
238 | return true;
239 | });
240 |
241 | dialog.close('test');
242 | });
243 |
244 | it('should add guard', () => {
245 | const guard = () => false;
246 |
247 | dialog.beforeClose(guard);
248 |
249 | expect(dialog.beforeCloseGuards).toEqual([guard]);
250 | });
251 |
252 | describe('should abort close', () => {
253 | it('using a sync function', () => {
254 | dialog.beforeClose(() => false);
255 |
256 | dialog.close();
257 |
258 | expect(dialogHasBeenClosed).toBeFalse();
259 | });
260 |
261 | it('using a promise', fakeAsync(() => {
262 | dialog.beforeClose(() => new Promise((r) => setTimeout(() => r(false), 1000)));
263 |
264 | dialog.close();
265 |
266 | tick(1000);
267 |
268 | expect(dialogHasBeenClosed).toBeFalse();
269 | }));
270 |
271 | it('using an observable', fakeAsync(() => {
272 | dialog.beforeClose(() => timer(1000).pipe(mapTo(false)));
273 |
274 | dialog.close();
275 |
276 | tick(1000);
277 |
278 | expect(dialogHasBeenClosed).toBeFalse();
279 | }));
280 |
281 | it('using more than one guard', fakeAsync(() => {
282 | dialog.beforeClose(() => false);
283 | dialog.beforeClose(() => new Promise((r) => setTimeout(() => r(false), 1000)));
284 | dialog.beforeClose(() => timer(1000).pipe(mapTo(false)));
285 |
286 | dialog.close();
287 |
288 | tick(1000);
289 |
290 | expect(dialogHasBeenClosed).toBeFalse();
291 | }));
292 |
293 | it('when only one guard returns false', fakeAsync(() => {
294 | dialog.beforeClose(() => true);
295 | dialog.beforeClose(() => new Promise((r) => setTimeout(() => r(false), 1000)));
296 | dialog.beforeClose(() => timer(3000).pipe(mapTo(true)));
297 |
298 | dialog.close();
299 |
300 | expect(dialogHasBeenClosed).toBeFalse();
301 |
302 | tick(1000);
303 |
304 | expect(dialogHasBeenClosed).toBeFalse();
305 |
306 | tick(2000);
307 |
308 | expect(dialogHasBeenClosed).toBeFalse();
309 | }));
310 | });
311 |
312 | it('should close dialog after all guards return true', fakeAsync(() => {
313 | dialog.beforeClose(() => true);
314 | dialog.beforeClose(() => new Promise((r) => setTimeout(() => r(true), 1000)));
315 | dialog.beforeClose(() => timer(3000).pipe(mapTo(true)));
316 |
317 | dialog.close();
318 |
319 | expect(dialogHasBeenClosed).toBeFalse();
320 |
321 | tick(1000);
322 |
323 | expect(dialogHasBeenClosed).toBeFalse();
324 |
325 | tick(2000);
326 |
327 | expect(dialogHasBeenClosed).toBeTrue();
328 | }));
329 | });
330 |
331 | describe('using closeAll', () => {
332 | let dialogHasBeenClosed: boolean;
333 |
334 | beforeEach(() => {
335 | dialogHasBeenClosed = false;
336 | dialog.afterClosed$.subscribe({ next: () => (dialogHasBeenClosed = true) });
337 | });
338 |
339 | it('should close if there are no guards', () => {
340 | expect(dialog.beforeCloseGuards).toEqual([]);
341 |
342 | service.closeAll();
343 |
344 | expect(dialogHasBeenClosed).toBeTrue();
345 | });
346 |
347 | describe('should abort close', () => {
348 | it('using a sync function', () => {
349 | dialog.beforeClose(() => false);
350 |
351 | service.closeAll();
352 |
353 | expect(dialogHasBeenClosed).toBeFalse();
354 | });
355 |
356 | it('using a promise', fakeAsync(() => {
357 | dialog.beforeClose(() => new Promise((r) => setTimeout(() => r(false), 1000)));
358 |
359 | service.closeAll();
360 |
361 | tick(1000);
362 |
363 | expect(dialogHasBeenClosed).toBeFalse();
364 | }));
365 |
366 | it('using an observable', fakeAsync(() => {
367 | dialog.beforeClose(() => timer(1000).pipe(mapTo(false)));
368 |
369 | service.closeAll();
370 |
371 | tick(1000);
372 |
373 | expect(dialogHasBeenClosed).toBeFalse();
374 | }));
375 |
376 | it('using more than one guard', fakeAsync(() => {
377 | dialog.beforeClose(() => false);
378 | dialog.beforeClose(() => new Promise((r) => setTimeout(() => r(false), 1000)));
379 | dialog.beforeClose(() => timer(1000).pipe(mapTo(false)));
380 |
381 | service.closeAll();
382 |
383 | tick(1000);
384 |
385 | expect(dialogHasBeenClosed).toBeFalse();
386 | }));
387 |
388 | it('when only one guard returns false', fakeAsync(() => {
389 | dialog.beforeClose(() => true);
390 | dialog.beforeClose(() => new Promise((r) => setTimeout(() => r(false), 1000)));
391 | dialog.beforeClose(() => timer(3000).pipe(mapTo(true)));
392 |
393 | service.closeAll();
394 |
395 | expect(dialogHasBeenClosed).toBeFalse();
396 |
397 | tick(1000);
398 |
399 | expect(dialogHasBeenClosed).toBeFalse();
400 |
401 | tick(2000);
402 |
403 | expect(dialogHasBeenClosed).toBeFalse();
404 | }));
405 | });
406 |
407 | it('should close dialog after all guards return true', fakeAsync(() => {
408 | dialog.beforeClose(() => true);
409 | dialog.beforeClose(() => new Promise((r) => setTimeout(() => r(true), 1000)));
410 | dialog.beforeClose(() => timer(3000).pipe(mapTo(true)));
411 |
412 | service.closeAll();
413 |
414 | expect(dialogHasBeenClosed).toBeFalse();
415 |
416 | tick(1000);
417 |
418 | expect(dialogHasBeenClosed).toBeFalse();
419 |
420 | tick(2000);
421 |
422 | expect(dialogHasBeenClosed).toBeTrue();
423 | }));
424 | });
425 |
426 | it('should remove dialog from dialogs', () => {
427 | dialog.close();
428 |
429 | expect(service.dialogs).toEqual([]);
430 | });
431 |
432 | it('should remove child from container', () => {
433 | dialog.close();
434 |
435 | expect(fakeDocument.body.removeChild).toHaveBeenCalledTimes(1);
436 | });
437 |
438 | it('should remove references from DialogRef', () => {
439 | dialog.close();
440 |
441 | const dialogCleaned: Partial = {
442 | id: dialog.id,
443 | data: dialog.data,
444 | afterClosed$: null,
445 | backdropClick$: null,
446 | beforeCloseGuards: null,
447 | onClose: null,
448 | ref: null,
449 | };
450 |
451 | expect(dialog).toEqual(jasmine.objectContaining(dialogCleaned));
452 | });
453 |
454 | it('should emit afterClosed$ and complete it', () => {
455 | let hasNext = false;
456 | let hasCompleted = false;
457 | dialog.afterClosed$.subscribe({ next: () => (hasNext = true), complete: () => (hasCompleted = true) });
458 |
459 | dialog.close();
460 |
461 | expect(hasNext).toBeTrue();
462 | expect(hasCompleted).toBeTrue();
463 | });
464 |
465 | it('should send result in afterClosed$', () => {
466 | dialog.afterClosed$.subscribe({ next: (result) => expect(result).toBe('test') });
467 |
468 | dialog.close('test');
469 | });
470 | });
471 |
472 | it('should use container to place dialog element', () => {
473 | const template = new FakeTemplateRef();
474 | const otherContainer = {
475 | appendChild: jasmine.createSpy(),
476 | };
477 |
478 | service.open(template, {
479 | container: otherContainer as any,
480 | });
481 |
482 | expect(fakeDocument.body.appendChild).not.toHaveBeenCalled();
483 | expect(otherContainer.appendChild).toHaveBeenCalledTimes(1);
484 | });
485 | });
486 |
--------------------------------------------------------------------------------
/libs/dialog/src/lib/specs/dialog.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core';
2 | import { createComponentFactory, Spectator } from '@ngneat/spectator';
3 | import { Subject } from 'rxjs';
4 |
5 | import { DialogCloseDirective } from '../dialog-close.directive';
6 | import { DialogService } from '../dialog.service';
7 |
8 | describe('Dialog', () => {
9 | @Component({
10 | selector: 'test-dialog',
11 | standalone: true,
12 | template: 'component dialog ',
13 | })
14 | class TestDialogComponent {
15 | changes$ = new Subject();
16 | }
17 |
18 | @Component({
19 | selector: 'test-dialog',
20 | standalone: true,
21 | imports: [DialogCloseDirective],
22 | template: 'Close using dialogClose ',
23 | })
24 | class TestDialogClosableUsingDialogCloseComponent {}
25 |
26 | @Component({
27 | standalone: true,
28 | imports: [DialogCloseDirective],
29 | template: `
30 |
31 |
32 | template dialog
33 |
34 |
35 |
36 |
37 |
38 |
39 | Close using dialogClose
40 |
41 |
42 |
43 | Close using dialogClose
44 |
45 | `,
46 | })
47 | class TestComponent {
48 | @ViewChild('otherVCR', { read: ViewContainerRef })
49 | otherVCR: ViewContainerRef;
50 |
51 | @ViewChild('tmpl')
52 | tmpl: TemplateRef;
53 |
54 | @ViewChild('usingDialogClose')
55 | tmplWithDialogClose: TemplateRef;
56 |
57 | @ViewChild('usingDialogCloseWithoutResult')
58 | tmplWithDialogCloseWithoutResult: TemplateRef;
59 |
60 | tmplChanges$ = new Subject();
61 |
62 | constructor(public dialog: DialogService) {}
63 | }
64 |
65 | let spectator: Spectator;
66 |
67 | const createComponent = createComponentFactory({
68 | component: TestComponent,
69 | imports: [TestDialogComponent, TestDialogClosableUsingDialogCloseComponent],
70 | });
71 |
72 | afterEach(() => {
73 | const dialogEls = document.querySelectorAll('ngneat-dialog');
74 |
75 | Array.from(dialogEls)
76 | .filter(Boolean)
77 | .forEach((el) => el.remove());
78 | });
79 |
80 | it('should create it', () => {
81 | spectator = createComponent();
82 |
83 | expect(spectator.component).toBeTruthy();
84 | });
85 |
86 | it('should open dialog with a template', () => {
87 | spectator = createComponent();
88 | const { component } = spectator;
89 |
90 | component.dialog.open(component.tmpl);
91 |
92 | spectator.detectChanges();
93 |
94 | expect(document.querySelector('#tmpl')).toContainText('template dialog');
95 | });
96 |
97 | it('should be able of subscribe to afterClosed$ and backdropClick$', () => {
98 | spectator = createComponent();
99 | const { component } = spectator;
100 |
101 | const dialogRef = component.dialog.open(component.tmpl);
102 |
103 | expect(() => dialogRef.afterClosed$.subscribe()).not.toThrow();
104 | expect(() => dialogRef.backdropClick$.subscribe()).not.toThrow();
105 | });
106 |
107 | it('should open dialog with a component', () => {
108 | spectator = createComponent();
109 | const { component } = spectator;
110 |
111 | component.dialog.open(TestDialogComponent);
112 |
113 | spectator.detectChanges();
114 |
115 | expect(document.querySelector('test-dialog')).toContainText('component dialog');
116 | });
117 |
118 | describe('should close using dialogClose directive', () => {
119 | it('into a template', () => {
120 | let value = false;
121 | spectator = createComponent();
122 | spectator.component.dialog.open(spectator.component.tmplWithDialogClose).afterClosed$.subscribe({
123 | next: (result) => (value = result),
124 | });
125 | spectator.detectChanges();
126 | spectator.click(spectator.query('#closeUsingDialog', { root: true }));
127 | expect(value).toBeTrue();
128 | });
129 | it('into a template without binding return empty string', () => {
130 | let value = 'should be empty';
131 | spectator = createComponent();
132 | spectator.component.dialog.open(spectator.component.tmplWithDialogCloseWithoutResult).afterClosed$.subscribe({
133 | next: (result) => (value = result),
134 | });
135 | spectator.detectChanges();
136 | spectator.click(spectator.query('#closeUsingDialog', { root: true }));
137 | expect(value).toBe('');
138 | });
139 | it('into a template using a view container ref', () => {
140 | let value = false;
141 | spectator = createComponent();
142 | spectator.component.dialog
143 | .open(spectator.component.tmplWithDialogClose, {
144 | vcr: spectator.component.otherVCR,
145 | })
146 | .afterClosed$.subscribe({
147 | next: (result) => (value = result),
148 | });
149 | spectator.detectChanges();
150 | spectator.click(spectator.query('#closeUsingDialog', { root: true }));
151 | expect(value).toBeTrue();
152 | });
153 | it('into a component', () => {
154 | let value = false;
155 | spectator = createComponent();
156 | spectator.component.dialog.open(TestDialogClosableUsingDialogCloseComponent).afterClosed$.subscribe({
157 | next: (result) => (value = result),
158 | });
159 | spectator.detectChanges();
160 | spectator.click(spectator.query('#closeUsingDialog', { root: true }));
161 | expect(value).toBeTrue();
162 | });
163 | it('into a component using a view container ref', () => {
164 | let value = false;
165 | spectator = createComponent();
166 | spectator.component.dialog
167 | .open(TestDialogClosableUsingDialogCloseComponent, {
168 | vcr: spectator.component.otherVCR,
169 | })
170 | .afterClosed$.subscribe({
171 | next: (result) => (value = result),
172 | });
173 | spectator.detectChanges();
174 | spectator.click(spectator.query('#closeUsingDialog', { root: true }));
175 | expect(value).toBeTrue();
176 | });
177 | });
178 | });
179 |
--------------------------------------------------------------------------------
/libs/dialog/src/lib/specs/types.spec.ts:
--------------------------------------------------------------------------------
1 | import { DialogRef } from '../dialog-ref';
2 | import { ExtractData, ExtractResult } from '../types';
3 |
4 | // Helper functions
5 |
6 | type Expect = T;
7 | type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false;
8 |
9 | // Type tests for ExtractData helper
10 |
11 | type WellDefinedStringData = ExtractData<{ ref: DialogRef }>;
12 | type WellDefinedObjectData = ExtractData<{ ref: DialogRef<{ key: string }> }>;
13 | type WellDefinedUnknownData = ExtractData<{ ref: DialogRef }>;
14 | type WellDefinedAnyData = ExtractData<{ ref: DialogRef }>;
15 | type ImplicitAnyData = ExtractData<{ ref: DialogRef }>;
16 | type MissingRefData = ExtractData<{}>;
17 |
18 | type dataCases = [
19 | Expect>,
20 | Expect>,
21 | Expect>,
22 | Expect>,
23 | Expect>,
24 | Expect>,
25 | ];
26 |
27 | // Type tests for ExtractResult helper
28 |
29 | type WellDefinedStringResult = ExtractResult<{ ref: DialogRef }>;
30 | type WellDefinedObjectResult = ExtractResult<{ ref: DialogRef }>;
31 | type WellDefinedUnknownResult = ExtractResult<{ ref: DialogRef }>;
32 | type WellDefinedAnyResult = ExtractResult<{ ref: DialogRef }>;
33 | type ImplicitAnyResult = ExtractResult<{ ref: DialogRef }>;
34 | type MissingRefResult = ExtractResult<{}>;
35 |
36 | type resultCases = [
37 | Expect>,
38 | Expect>,
39 | Expect>,
40 | Expect>,
41 | Expect>,
42 | Expect>,
43 | ];
44 |
--------------------------------------------------------------------------------
/libs/dialog/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | import { ComponentRef, ElementRef, TemplateRef, ViewContainerRef, ViewRef } from '@angular/core';
2 |
3 | import { DialogRef } from './dialog-ref';
4 |
5 | type Sizes = 'sm' | 'md' | 'lg' | 'fullScreen' | string;
6 | export type DragConstraint = 'none' | 'bounce' | 'constrain';
7 | export type CloseStrategy = boolean | 'onlyLastStrategy';
8 |
9 | export interface GlobalDialogConfig {
10 | sizes: Partial<
11 | Record<
12 | Sizes,
13 | {
14 | width?: string | number;
15 | minWidth?: string | number;
16 | maxWidth?: string | number;
17 | height?: string | number;
18 | minHeight?: string | number;
19 | maxHeight?: string | number;
20 | }
21 | >
22 | >;
23 | backdrop: boolean;
24 | container: ElementRef | Element;
25 | closeButton: boolean;
26 | draggable: boolean;
27 | dragConstraint: DragConstraint;
28 | enableClose:
29 | | CloseStrategy
30 | | {
31 | escape: CloseStrategy;
32 | backdrop: CloseStrategy;
33 | };
34 | resizable: boolean;
35 | overflow: boolean;
36 | width: string | number;
37 | minWidth: string | number;
38 | maxWidth: string | number;
39 | height: string | number;
40 | minHeight: string | number;
41 | maxHeight: string | number;
42 | size: Sizes;
43 | windowClass: string;
44 | zIndexGetter?(): number;
45 | onOpen: () => void | undefined;
46 | onClose: () => void | undefined;
47 | }
48 |
49 | export interface DialogConfig extends Omit {
50 | id: string;
51 | data: Data;
52 | vcr: ViewContainerRef;
53 | }
54 |
55 | export type JustProps = Pick<
56 | T,
57 | {
58 | [K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;
59 | }[keyof T]
60 | >;
61 |
62 | export type ExtractRefProp = Exclude<
63 | {
64 | [P in keyof T]: T[P] extends DialogRef ? P : never;
65 | }[keyof T],
66 | undefined | null
67 | >;
68 |
69 | export type ExtractData = ExtractRefProp extends never
70 | ? any
71 | : T[ExtractRefProp] extends DialogRef
72 | ? Data
73 | : never;
74 | export type ExtractResult = ExtractRefProp extends never
75 | ? any
76 | : T[ExtractRefProp] extends DialogRef
77 | ? Result
78 | : never;
79 |
80 | export interface AttachOptions {
81 | ref: ComponentRef | TemplateRef;
82 | view: ViewRef;
83 | attachToApp: boolean;
84 | }
85 |
--------------------------------------------------------------------------------
/libs/dialog/src/public-api.ts:
--------------------------------------------------------------------------------
1 | export { DialogService } from './lib/dialog.service';
2 | export { DialogRef } from './lib/dialog-ref';
3 | export { provideDialogConfig, provideDialogDocRef } from './lib/providers';
4 | export { DialogCloseDirective } from './lib/dialog-close.directive';
5 | export { DialogConfig } from './lib/types';
6 | export { CloseAllDialogsDirective } from './lib/close-all-dialogs.directive';
7 |
--------------------------------------------------------------------------------
/libs/dialog/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js';
4 | import 'zone.js/testing';
5 | import { getTestBed } from '@angular/core/testing';
6 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
7 |
8 | // First, initialize the Angular testing environment.
9 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
10 | teardown: { destroyAfterEach: false },
11 | });
12 |
--------------------------------------------------------------------------------
/libs/dialog/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "../../../dist/out-tsc",
5 | "declarationMap": true,
6 | "declaration": true,
7 | "inlineSources": true,
8 | "types": [],
9 | "lib": ["dom", "es2022"]
10 | },
11 | "angularCompilerOptions": {
12 | "skipTemplateCodegen": true,
13 | "strictMetadataEmit": true,
14 | "enableResourceInlining": true
15 | },
16 | "exclude": ["src/test.ts", "**/*.spec.ts"],
17 | "include": ["**/*.ts"]
18 | }
19 |
--------------------------------------------------------------------------------
/libs/dialog/tsconfig.lib.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.lib.json",
3 | "compilerOptions": {
4 | "declarationMap": false
5 | },
6 | "angularCompilerOptions": {
7 | "compilationMode": "partial"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/libs/dialog/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "../../../dist/out-tsc",
5 | "types": ["jasmine", "node"]
6 | },
7 | "files": ["src/test.ts"],
8 | "include": ["**/*.spec.ts", "**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/migrations.json:
--------------------------------------------------------------------------------
1 | {
2 | "migrations": [
3 | {
4 | "version": "16.0.0",
5 | "factory": "./update-16/remove-default-project-option",
6 | "description": "Remove 'defaultProject' option from workspace configuration. The project to use will be determined from the current working directory.",
7 | "package": "@angular/cli",
8 | "name": "remove-default-project-option"
9 | },
10 | {
11 | "version": "16.0.0",
12 | "factory": "./update-16/replace-default-collection-option",
13 | "description": "Replace removed 'defaultCollection' option in workspace configuration with 'schematicCollections'.",
14 | "package": "@angular/cli",
15 | "name": "replace-default-collection-option"
16 | },
17 | {
18 | "version": "16.0.0",
19 | "factory": "./update-16/update-server-builder-config",
20 | "description": "Update the '@angular-devkit/build-angular:server' builder configuration to disable 'buildOptimizer' for non optimized builds.",
21 | "package": "@angular/cli",
22 | "name": "update-server-builder-config"
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "affected": {
3 | "defaultBase": "main"
4 | },
5 | "namedInputs": {
6 | "sharedGlobals": [],
7 | "default": ["{projectRoot}/**/*", "sharedGlobals"],
8 | "production": [
9 | "default",
10 | "!{projectRoot}/tsconfig.spec.json",
11 | "!{projectRoot}/**/*.spec.[jt]s",
12 | "!{projectRoot}/karma.conf.js"
13 | ]
14 | },
15 | "targetDefaults": {
16 | "build": {
17 | "dependsOn": ["^build"],
18 | "inputs": ["production", "^production"],
19 | "cache": true
20 | },
21 | "test": {
22 | "inputs": ["default", "^production", "{workspaceRoot}/karma.conf.js"],
23 | "cache": true
24 | },
25 | "e2e": {
26 | "inputs": ["default", "^production"],
27 | "cache": true
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dialog",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "nx serve playground",
7 | "build": "nx build playground --prod",
8 | "deploy": "nx deploy --base-href=https://ngneat.github.io/dialog/",
9 | "hooks:pre-commit": "node scripts/pre-commit.js",
10 | "commit": "git-cz",
11 | "build:lib": "nx build dialog --configuration production && node scripts/post-build.js",
12 | "test:lib": "nx test dialog",
13 | "release": "nx run dialog:version",
14 | "ci:test": "npm run test:lib",
15 | "ci:lint": "nx run-many --target=lint",
16 | "prepare": "husky install",
17 | "migration:run": "nx migrate --run-migrations",
18 | "migrate": "nx migrate latest"
19 | },
20 | "private": true,
21 | "dependencies": {
22 | "@angular/animations": "^17.0.4",
23 | "@angular/common": "^17.0.4",
24 | "@angular/compiler": "^17.0.4",
25 | "@angular/core": "^17.0.4",
26 | "@angular/forms": "^17.0.4",
27 | "@angular/platform-browser": "^17.0.4",
28 | "@angular/platform-browser-dynamic": "^17.0.4",
29 | "@angular/router": "^17.0.4",
30 | "@ngneat/overview": "6.0.0",
31 | "rxjs": "~7.5.0",
32 | "tslib": "2.3.1",
33 | "zone.js": "~0.14.2"
34 | },
35 | "devDependencies": {
36 | "@angular-devkit/build-angular": "^17.0.3",
37 | "@angular/cli": "^17.0.3",
38 | "@angular/compiler-cli": "^17.0.4",
39 | "@angular-devkit/core": "16.2.10",
40 | "@angular-devkit/schematics": "16.2.10",
41 | "@angular-eslint/eslint-plugin": "^16.3.1",
42 | "@angular-eslint/eslint-plugin-template": "^16.3.1",
43 | "@angular-eslint/template-parser": "^16.3.1",
44 | "@commitlint/cli": "18.4.1",
45 | "@commitlint/config-angular": "18.4.0",
46 | "@commitlint/config-conventional": "18.4.0",
47 | "@jscutlery/semver": "4.0.0",
48 | "@ngneat/spectator": "15.0.1",
49 | "@nx/angular": "17.1.3",
50 | "@nx/eslint-plugin": "17.1.3",
51 | "@nx/js": "17.1.3",
52 | "@nx/linter": "17.1.3",
53 | "@nx/workspace": "17.1.3",
54 | "@schematics/angular": "^15.2.7",
55 | "@swc-node/register": "~1.6.7",
56 | "@swc/core": "~1.3.85",
57 | "@types/jasmine": "4.3.0",
58 | "@types/node": "18.17.18",
59 | "@typescript-eslint/eslint-plugin": "^6.10.0",
60 | "@typescript-eslint/parser": "^6.10.0",
61 | "angular-cli-ghpages": "1.0.7",
62 | "chalk": "4.1.2",
63 | "cross-env": "^5.2.0",
64 | "eslint": "^8.53.0",
65 | "eslint-config-prettier": "^9.0.0",
66 | "eslint-import-resolver-typescript": "^3.6.1",
67 | "eslint-plugin-import": "^2.29.0",
68 | "git-cz": "4.9.0",
69 | "husky": "8.0.3",
70 | "jasmine-core": "5.1.1",
71 | "jasmine-spec-reporter": "7.0.0",
72 | "karma": "6.4.2",
73 | "karma-chrome-launcher": "3.2.0",
74 | "karma-coverage-istanbul-reporter": "3.0.3",
75 | "karma-jasmine": "5.1.0",
76 | "karma-jasmine-html-reporter": "2.1.0",
77 | "lint-staged": "15.1.0",
78 | "ng-packagr": "^17.0.2",
79 | "nx": "17.1.3",
80 | "postcss": "8.4.31",
81 | "postcss-import": "15.1.0",
82 | "postcss-preset-env": "9.3.0",
83 | "postcss-url": "10.1.3",
84 | "prettier": "3.0.3",
85 | "typescript": "~5.2.2"
86 | },
87 | "config": {
88 | "commitizen": {
89 | "path": "cz-conventional-changelog"
90 | }
91 | },
92 | "lint-staged": {
93 | "*.{js,json,css,scss,ts,html,component.html}": [
94 | "prettier --write"
95 | ]
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | singleQuote: true,
3 | tabWidth: 2,
4 | useTabs: false,
5 | printWidth: 120,
6 | };
7 |
--------------------------------------------------------------------------------
/scripts/post-build.js:
--------------------------------------------------------------------------------
1 | const fs = require('node:fs');
2 |
3 | fs.copyFileSync('README.md', 'dist/libs/dialog/README.md');
4 |
--------------------------------------------------------------------------------
/scripts/pre-commit.js:
--------------------------------------------------------------------------------
1 | const { execSync } = require('node:child_process');
2 | const chalk = require('chalk');
3 |
4 | /** Map of forbidden words and their match regex */
5 | const words = {
6 | fit: '\\s*fit\\(',
7 | fdescribe: '\\s*fdescribe\\(',
8 | debugger: '(debugger);?',
9 | };
10 | let status = 0;
11 | for (let word of Object.keys(words)) {
12 | const matchRegex = words[word];
13 | const gitCommand = `git diff --staged -G"${matchRegex}" --name-only`;
14 | const badFiles = execSync(gitCommand).toString();
15 | const filesAsArray = badFiles.split('\n');
16 | const tsFileRegex = /\.ts$/;
17 | const onlyTsFiles = filesAsArray.filter((file) => tsFileRegex.test(file.trim()));
18 | if (onlyTsFiles.length) {
19 | status = 1;
20 | console.log(chalk.bgRed.black.bold(`The following files contains '${word}' in them:`));
21 | console.log(chalk.bgRed.black(onlyTsFiles.join('\n')));
22 | }
23 | }
24 | process.exit(status);
25 |
--------------------------------------------------------------------------------
/tools/generators/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngneat/dialog/0153edfd31903b3a0076be89e27de0edbf9b48f5/tools/generators/.gitkeep
--------------------------------------------------------------------------------
/tools/tsconfig.tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "../dist/out-tsc/tools",
5 | "rootDir": ".",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "types": ["node"],
9 | "importHelpers": false
10 | },
11 | "include": ["**/*.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "downlevelIteration": true,
9 | "experimentalDecorators": true,
10 | "module": "es2020",
11 | "moduleResolution": "node",
12 | "importHelpers": true,
13 | "target": "ES2022",
14 | "lib": ["es2022", "dom"],
15 | "paths": {
16 | "@ngneat/dialog": ["libs/dialog/src/public-api.ts"]
17 | },
18 | "useDefineForClassFields": false,
19 | "rootDir": "."
20 | },
21 | "angularCompilerOptions": {
22 | "enableI18nLegacyMessageIdFormat": false,
23 | "strictInjectionParameters": true,
24 | "strictInputAccessModifiers": true,
25 | "strictTemplates": true
26 | },
27 | "exclude": ["node_modules", "tmp"]
28 | }
29 |
--------------------------------------------------------------------------------