Skip to content

Commit e4dfde8

Browse files
committed
feat(ngx-inform): Add modal setup
1 parent 669d803 commit e4dfde8

22 files changed

+581
-10
lines changed

apps/layout-test/src/app/app.component.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,7 @@
1313

1414
<hr />
1515

16+
<button (click)="sayHello()">Say hello!</button>
17+
<button (click)="confirm()">Confirm!</button>
18+
1619
<router-outlet/>
Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { Component } from '@angular/core';
22

33
import { RouterModule } from '@angular/router';
4+
import { tap } from 'rxjs';
5+
import { ModalComponent } from '../modal/modal.component';
46
import { NgxMediaQueryService } from '@ngx/utils';
7+
import { NgxModalService } from '@ngx/inform';
58

69
@Component({
710
selector: 'app-root',
@@ -10,12 +13,42 @@ import { NgxMediaQueryService } from '@ngx/utils';
1013
imports: [RouterModule],
1114
})
1215
export class AppComponent {
13-
constructor(private readonly mediaService: NgxMediaQueryService) {
16+
constructor(
17+
private readonly mediaService: NgxMediaQueryService,
18+
private readonly modalService: NgxModalService
19+
) {
1420
// Wouter: To see these in action, navigate to '/queries' in the browser
1521
this.mediaService.registerMediaQueries(
1622
['small', '(max-width: 500px)'],
1723
['medium', '(max-width: 600px)'],
1824
['large', '(max-width: 700px)']
1925
);
2026
}
27+
28+
public sayHello(): void {
29+
this.modalService
30+
.open({
31+
component: ModalComponent,
32+
label: 'Modal',
33+
role: 'dialog',
34+
})
35+
.pipe(
36+
tap((action) => {
37+
if (action === 'Test') {
38+
console.log('Hello!');
39+
}
40+
})
41+
)
42+
.subscribe();
43+
}
44+
45+
confirm(): void {
46+
this.modalService
47+
.open({
48+
type: 'confirm',
49+
describedById: 'id',
50+
labelledById: 'hello',
51+
})
52+
.subscribe();
53+
}
2154
}

apps/layout-test/src/app/app.config.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import {
88
import { TourItemComponent } from '../tour/tour.component';
99
import { routes } from '../routes';
1010
import { TooltipComponent } from '../tooltip/tooltip.component';
11+
import { ConfirmModalComponent } from '../modal/confirm.component';
1112
import { provideNgxDisplayContentConfiguration } from '@ngx/layout';
1213
import { provideNgxTourConfiguration } from '@ngx/tour';
13-
import { provideNgxTooltipConfiguration } from '@ngx/inform';
14+
import { provideNgxModalConfiguration, provideNgxTooltipConfiguration } from '@ngx/inform';
1415

1516
export const appConfig: ApplicationConfig = {
1617
providers: [
@@ -25,6 +26,14 @@ export const appConfig: ApplicationConfig = {
2526
}),
2627
provideNgxTourConfiguration(TourItemComponent),
2728
provideNgxTooltipConfiguration({ component: TooltipComponent }),
29+
provideNgxModalConfiguration({
30+
modals: {
31+
confirm: {
32+
component: ConfirmModalComponent,
33+
role: 'alertdialog',
34+
},
35+
},
36+
}),
2837
provideRouter(routes),
2938
],
3039
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Component } from '@angular/core';
2+
import { NgxModalAbstractComponent } from '@ngx/inform';
3+
4+
@Component({
5+
selector: 'confirm-modal',
6+
template: `
7+
Confirm this please
8+
<button (click)="action.emit('Confirm')">Confirm</button>
9+
<button (click)="close.emit()">Close</button>
10+
`,
11+
styleUrl: './modal.component.scss',
12+
standalone: true,
13+
})
14+
export class ConfirmModalComponent extends NgxModalAbstractComponent<'Confirm'> {}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
:host {
2+
display: block;
3+
background: white;
4+
border: 1px solid black;
5+
padding: 15px;
6+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Component } from '@angular/core';
2+
import { NgxModalAbstractComponent } from '@ngx/inform';
3+
4+
@Component({
5+
selector: 'test-modal',
6+
template: `
7+
Hello world!
8+
<button (click)="action.emit('Test')">Hello there!</button>
9+
<button (click)="close.emit()">Close</button>
10+
`,
11+
styleUrl: './modal.component.scss',
12+
standalone: true,
13+
})
14+
export class ModalComponent extends NgxModalAbstractComponent<'Test'> {}

libs/inform/README.md

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ For more information about the build process, authors, contributions and issues,
2121

2222
## Concept
2323

24-
`ngx-inform` is a package to help facilitate common user information use-cases such as tooltips, toasts and snackbars.
24+
`ngx-inform` is a package to help facilitate common user information use-cases such as tooltips, toasts and snackbars.
2525

26-
Currently the package provides a `ngxTooltip` directive which can be used to attach a customizable ARIA compliant tooltip to any component.
26+
At its core, `ngx-inform` is build to be WCAG and ARIA compliant, and will throw errors whenever the necessary setup has not been provided to ensure said compliancy.
27+
28+
Currently the package provides a `ngxTooltip` directive which can be used to attach a customizable ARIA compliant tooltip to any component and the `ngxModalService` which allows for both custom and predefined global modals to be used throughout the application.
2729

2830
## Directives
2931

@@ -85,3 +87,105 @@ On top of these two inputs, we have two additional inputs, being `ngxTooltipComp
8587
```
8688

8789
When you wish to disable a tooltip and thus prevent it from being shown, you can use the `ngxTooltipDisabled` property. By default, this property is false.
90+
91+
## Services
92+
93+
### NgxModalService
94+
95+
The `NgxModalService` provides a WCAG/ARIA compliant approach to the Angular CDK `Dialog` service. It is important to understand that, unlike the Dialog service of the CDK, `ngx-inform` **will enforce WCAG/ARIA compliance**. Because of that, certain configuration of the CDK Dialog becomes mandatory and other options that would result in incompliance have been disabled.
96+
97+
### Setup
98+
99+
You can use the `NgxModalService` without any prior setup, but the `ngx-inform` package does provide the ability to provide a global configuration that can be applied for all modals. On top of that, you can provide default modals with your specific configuration.
100+
101+
``` ts
102+
provideNgxModalConfiguration({
103+
closeOnNavigation: true,
104+
autoClose: true
105+
modals: {
106+
confirm: {
107+
component: ConfirmModalComponent,
108+
role: 'alertdialog',
109+
panelClass: 'panel-confirm',
110+
},
111+
},
112+
}),
113+
```
114+
115+
Using the above configuration, we can set several properties that will be applied to the modals globally. These properties are:
116+
| Property | |
117+
| ------------------------- | --------------------------------------------------------------------------------------------------------------- |
118+
| closeOnNavigation | Whether the modal closes on navigation, by default this is `true` |
119+
| direction | The reading direction of the modal. |
120+
| hasBackdrop | Whether or not we wish to set a backdrop, by default this is `true`. |
121+
| panelClass | A class set to the `overlay` element |
122+
| autoClose | Whether the modal automatically closes after the initial interaction emitted by the `action` output. By default this is `true`. |
123+
124+
On top of that, by passing a `modals` record, we can define a set preset modals we can use throughout the entire application. We can setup default modals for confirmation, navigating away from a route, etc. Next to overwrites of the global properties above, we can also provide the following properties:
125+
126+
| Property | |
127+
| ------------------------- | --------------------------------------------------------------------------------------------------------------- |
128+
| role | The ARIA role of the modal, either `dialog` or `alertDialog` |
129+
| component | An implementation of the `NgxModalAbstractComponent` |
130+
| data | Any data we wish to provide to the component. |
131+
132+
### Implementation
133+
134+
The `NgxModalService` allows for two ways of opening a modal. Either by opening a predefined modal we set in the configuration, or a custom modal by passing a new modal component.
135+
136+
To make the modals ARIA compliant either a `label` or a `labelledById` must be provided. If the role of a modal was set to `alertdialog`, the `describedById` property is also required.
137+
138+
When opening a modal we can overwrite all the globally and modal-specific configuration using the same properties as mentioned earlier. On top of that, we can set several other properties. These properties are:
139+
140+
| Property | |
141+
| ------------------------- | --------------------------------------------------------------------------------------------------------------- |
142+
| injector | Injector used for the instantiation of the component to be attached. If provided, takes precedence over the injector indirectly provided by ViewContainerRef. |
143+
| viewContainerRef | Where the attached component should live in Angular's logical component tree. This affects what is available for injection and the change detection order for the component instantiated inside of the dialog. This does not affect where the dialog content will be rendered. |
144+
| restoreFocus | Whether the dialog should restore focus to the previously-focused element upon closing.|
145+
| autoFocus | Where the dialog should focus on open. |
146+
147+
#### Predefined modal
148+
149+
If we set a predefined modal, we can now call said modal using the `open` method on `NgxModalService`.
150+
151+
```ts
152+
this.modalService
153+
.open<'Confirm' | 'Deny'>({
154+
type: 'confirm',
155+
describedById: 'confirm-button',
156+
labelledById: 'confirm-label',
157+
data: {
158+
title: 'Please confirm your actions!'
159+
}
160+
})
161+
.pipe(
162+
tap(action => {
163+
if(action === 'Confirm') {
164+
// Perform confirm logic
165+
}
166+
167+
// Perform non-confirm logic
168+
})
169+
)
170+
.subscribe();
171+
```
172+
#### Custom modal
173+
174+
We can always create a custom modal for feature-specific use-cases. We do this by providing a component.
175+
176+
``` ts
177+
this.modalService
178+
.open<'Test'>({
179+
component: ModalComponent,
180+
label: 'Modal',
181+
role: 'dialog',
182+
})
183+
.pipe(
184+
tap((action) => {
185+
if (action === 'Test') {
186+
console.log('Hello!');
187+
}
188+
})
189+
)
190+
.subscribe();
191+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './tooltip/tooltip.abstract.component';
2+
export * from './modal/modal.abstract.component';
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core';
2+
3+
/**
4+
* An abstract for the NgxModalService
5+
*/
6+
@Directive()
7+
export class NgxModalAbstractComponent<ActionType extends string = string, DataType = any> {
8+
/**
9+
* Remove the modal on escape pressed
10+
*/
11+
@HostListener('document:keydown.escape') onEscape() {
12+
this.close.emit();
13+
}
14+
15+
/**
16+
* Optional data that can be passed to the modal
17+
*/
18+
@Input() public data: DataType;
19+
20+
/**
21+
* An emitter that will emit an action we can later respond to
22+
*/
23+
@Output() public action: EventEmitter<ActionType> = new EventEmitter<ActionType>();
24+
25+
/**
26+
* An emitter that will emit if the modal is closed
27+
*/
28+
@Output() public close: EventEmitter<void> = new EventEmitter<void>();
29+
}

libs/inform/src/lib/abstracts/tooltip/tooltip.abstract.component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { Directive, HostBinding, HostListener, Input } from '@angular/core';
33
import { NgxTooltipPosition, NgxTooltipPositionClass } from '../../types';
44
import { NgxTooltipService } from '../../services';
55

6+
/**
7+
* An abstract for the NgxTooltipDirective
8+
*/
69
@Directive()
710
export abstract class NgxTooltipAbstractComponent {
811
/**

0 commit comments

Comments
 (0)