Skip to content

Commit

Permalink
Enhancement/modal close from parent (v4) (#721)
Browse files Browse the repository at this point in the history
* Add support for disable closing, and for closing modal by a method call from the parent in FHI Modal.
  • Loading branch information
proand authored Oct 17, 2024
1 parent b467cb5 commit c0d3c5d
Show file tree
Hide file tree
Showing 8 changed files with 1,336 additions and 1,081 deletions.
2,090 changes: 1,023 additions & 1,067 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions projects/fhi-angular-components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

> Oct 11, 2024
* :tada: **Enhancement** Add support for disable closing, and closing `fhi-modal` by a method call from the parent.
* :bug: **Bugfix** Make sure there is no duplicate IDs on checkboxes (or radios) if more than one instance of `FhiTreeViewCheckboxComponent` or `FhiTreeViewRadioComponent` on the same page.

## 4.4.2
Expand Down
2 changes: 1 addition & 1 deletion projects/fhi-angular-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@folkehelseinstituttet/angular-components",
"version": "4.4.2",
"publishConfig": {
"tag": "latest"
"tag": "v4"
},
"repository": {
"type": "git",
Expand Down
48 changes: 38 additions & 10 deletions projects/fhi-angular-components/src/lib/fhi-modal/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
# FHI Modal
# FHI Modal <!-- omit from toc -->

- [API](#api)
- [Input](#input)
- [Output](#output)
- [FhiModalActionButton](#fhimodalactionbutton)
- [How To](#how-to)
- [Keep modal open while waiting for server](#keep-modal-open-while-waiting-for-server)
- [Destroy services or reset forms when closing modal](#destroy-services-or-reset-forms-when-closing-modal)

## API

### Input

| Input | Type | Default | Required | Description |
| ----------------------- | ------------------------ | -------------- | -------- | ----------- |
| `[actionButtons]` | `FhiModalActionButton[]` | - | no | Object defining the buttons in the modal. |
| `modalTitle` | `string` | - | no | Title at the top of the modal. |
| `openModalButtonClass` | `string` | `fhi-btn-link` | no | Button class on the button that opens the modal. The button can also be modified with markup inside the button. |
| `[openModalFromParent]` | `boolean` | `false` | no | When set to `true` the method `FhiModalComponent.modal.open()` will be triggered. To be able to trigger change detection more than once, parent component is responsible for toggeling the value. |
| `[scrollable]` | `boolean` | `true` | no | Same as NgbModal |
| `size` | `string` | `md` | no | Same as NgbModal |
| Input | Type | Default | Required | Description |
| ------------------------ | ------------------------ | -------------- | -------- | ----------- |
| `[actionButtons]` | `FhiModalActionButton[]` | - | no | Object defining the buttons in the modal. |
| `modalTitle` | `string` | - | no | Title at the top of the modal. |
| `openModalButtonClass` | `string` | `fhi-btn-link` | no | Button class on the button that opens the modal. The button can also be modified with markup inside the button. |
| `[openModalFromParent]` | `boolean` | `false` | no | When set to `true`* the method `FhiModalComponent.modal.open()` will be called. |
| `[closeModalFromParent]` | `boolean` | `false` | no | When set to `true`* the method `FhiModalComponent.modal.dismissAll()` will be called. |
| `[disableCloseOnAction]` | `boolean` | `false` | no | When set to `true`* the method `FhiModalComponent.modal.dismissAll()` will NOT be called when `FhiModalComponent.onModalAction()` is called. |
| `[scrollable]` | `boolean` | `true` | no | Same as NgbModal |
| `size` | `string` | `md` | no | Same as NgbModal |

*\* To be able to trigger change detection more than once, parent component is responsible for toggeling the value.*

### Output

Expand All @@ -27,4 +39,20 @@
| ---------- | --------- | ------- | -------- | ----------- |
| `name` | `string` | - | yes | Name of the button calling the action. |
| `disabled` | `boolean` | - | no | Whether the button is disabled or not. |
| `primary` | `boolean` | - | no | Whether the button calls the primary action or not. |
| `primary`* | `boolean` | - | no | Whether the button calls the primary action or not. |

\* **NB!** Property `primary` will be deprecated in v6.0.0 of [@folkehelseinstituttet/angular-components](https://www.npmjs.com/package/@folkehelseinstituttet/angular-components).

## How To

Code demonstrating the use cases below is located [here](https://github.com/folkehelseinstituttet/Fhi.Frontend.Demo/tree/dev/src/app/views/shared/dynamic-library-examples/example-components/modals) (see example 5).

### Keep modal open while waiting for server

If the modal should be kept open while waiting for a server response, the input `disableCloseOnAction` must be set to `true`. The parent component must then take responsebility for closing the modal. This is done by setting input `closeModalFromParent` to `true`.

**NB!** To trigger change detection every time input `closeModalFromParent` is set to `true`, it must be set to `false` in between, so the parent component will have to do some state management. The same logic applies to inputs `openModalFromParent` btw.

### Destroy services or reset forms when closing modal

If the template content inside `<ng-container fhi-modal.body></ng-container>` contains a child component, a form or some service logic, and it's necessary to destroy component, reset form or do some other clean up when the modal is closed, this is the responsibility of the parent component. In example 5 this is demonstrated by a form reset (see method `onDismissModalExample5()`).
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export class FhiModalComponent implements OnChanges {
@Input() modalTitle!: string;
@Input() openModalButtonClass = 'fhi-btn-link';
@Input() openModalFromParent = false;
@Input() closeModalFromParent = false;
@Input() disableCloseOnAction = false;
@Input() scrollable = true;
@Input() size = 'md';

Expand All @@ -50,6 +52,9 @@ export class FhiModalComponent implements OnChanges {
if (changes['openModalFromParent']?.currentValue === true) {
this.modalOpen(this.modalContentRef);
}
if (changes['closeModalFromParent']?.currentValue === true) {
this.modal.dismissAll();
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -59,7 +64,9 @@ export class FhiModalComponent implements OnChanges {

onModalAction(button: FhiModalActionButton) {
this.modalAction.emit(button.name);
this.modal.dismissAll();
if (!this.disableCloseOnAction) {
this.modal.dismissAll();
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
4 changes: 4 additions & 0 deletions src/MOCK_DB_DATA/library-items/modals/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ function getCodeHtml(): string | null {
<fhi-modal [openModalFromParent]="openModal" (dismissModal)="onDismissModal()">
<ng-container fhi-modal.body><p>Modalvindu innhold.</p></ng-container>
</fhi-modal>
<!-- Eks. 5
For å se koden til skjemaeksemplet, gå til demokoden som det lenkes til under "Nyttige lenker" lenger opp på siden.</p>
-->
`;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<!-- Eks. 1 -->

<fhi-modal>
<ng-container fhi-modal.button>Åpne modal</ng-container>
<ng-container fhi-modal.body><p>Modalvindu innhold.</p></ng-container>
</fhi-modal>
<br />

<!-- Eks. 2 -->
<fhi-modal
[modalTitle]="'Overskrift'"
[size]="'sm'"
Expand All @@ -15,6 +18,8 @@
</fhi-modal>
<br /><br />

<!-- Eks. 3 -->

<fhi-modal
[modalTitle]="'Overskrift'"
[actionButtons]="actionButtons"
Expand All @@ -32,11 +37,128 @@
</fhi-modal>
<br /><br />

<!-- Eks. 4 -->

<fhi-popover-menu
[items]="[{ name: 'Åpne modal fra foreldrekomponent', action: 'openModal' }]"
(actionEvent)="onPopoverActionOpenModalFromParent($event)"
></fhi-popover-menu>

<fhi-modal [openModalFromParent]="openModal" (dismissModal)="onDismissModal()">
<ng-container fhi-modal.body><p>Modalvindu innhold.</p></ng-container>
</fhi-modal>
<br /><br />

<!-- Eks. 5 -->

<fhi-modal
[modalTitle]="'Overskrift'"
[size]="'sm'"
[actionButtons]="[{ name: 'Send' }]"
[closeModalFromParent]="example5.closeModal"
[disableCloseOnAction]="true"
(modalAction)="onModalActionExample5($event)"
(dismissModal)="onDismissModalExample5()"
>
<ng-container fhi-modal.button>Åpne modal med skjema</ng-container>
<ng-container fhi-modal.body>
<form class="mb-1" autocomplete="off" [formGroup]="example5.form">
<div class="mb-4">
<label for="firstName" class="form-label">Fornavn *</label>
<p class="form-text">Fornavn FHIs direktør</p>
<input
type="text"
id="firstName"
class="form-control"
ngbAutofocus
formControlName="firstName"
[ngClass]="{ 'is-invalid': firstName.invalid && (firstName.dirty || firstName.touched) }"
placeholder="Fornavn"
(keydown.enter)="onEnterExample5()"
/>
<p class="invalid-feedback">
@if (firstName.hasError('minlength') || firstName.hasError('maxlength')) {
"{{ firstName.value }}" er ikke riktig antall tegn.
} @else if (firstName.hasError('nameError')) {
"{{ firstName.value }}" er ikke fornavnet på FHIs direktør.
}
</p>
</div>

<div class="mb-4">
<label for="lastName" class="form-label">Etternavn</label>
<input
type="text"
id="lastName"
class="form-control"
formControlName="lastName"
[ngClass]="{ 'is-invalid': lastName.invalid && (lastName.dirty || lastName.touched) }"
placeholder="Etternavn"
/>
<p class="invalid-feedback">
@if (lastName.hasError('minlength') || lastName.hasError('maxlength')) {
"{{ lastName.value }}" er ikke riktig antall tegn.
} @else if (lastName.hasError('nameError')) {
"{{ lastName.value }}" er ikke etternavnet på FHIs direktør.
}
</p>
</div>

<label class="form-label">Server respons</label>
<div class="form-check mb-2">
<input
class="form-check-input"
type="radio"
id="serverResponsOk"
name="serverRespons"
formControlName="serverRespons"
[value]="'OK'"
/>
<label class="form-check-label" for="serverResponsOk">OK</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="radio"
id="serverResponsError"
name="serverRespons"
formControlName="serverRespons"
[value]="'error'"
/>
<label class="form-check-label" for="serverResponsError">Error</label>
</div>
</form>

<div *ngIf="example5.waitingForServer || example5.waitingForAsyncValidation">
<div class="alert alert-info mt-3" role="alert">
<i class="icon-info-circle"></i>
@if (example5.waitingForServer) {
Venter på server...
}
@if (example5.waitingForAsyncValidation) {
Venter på validering...
}
<div class="spinner-border spinner-border-sm ms-3" role="status"></div>
</div>
</div>

<div *ngIf="example5.serverError">
<div class="alert alert-error mt-3" role="alert">
<i class="icon-exclamation-circle"></i>
Velg server respons OK og prøv igjen.
</div>
</div>
</ng-container>
</fhi-modal>

<p class="mt-3">
<strong>Åpne modal med skjema</strong> er et litt mer komplekst eksempel som viser hvordan en kan trigge en handling
ved å klikke på <em>Enter</em> i input-felt, samt sørge for at modal ikke lukker seg før foreldrekomponenten gir
klarsignal. Eksemplet viser noe av mulighetsrommet for validering og error-håndtering, men må ikke sees på som noen
fasit, en må i hver løsning finne ut hva som fungerer best på ulike skjema.
</p>
<div class="alert alert-warning mt-3" role="alert">
<i class="icon-bell"></i>
For å få skjema til å fungere stabilt sammen med modal kan en ikke bruke Angulars innebygde output-event (ngSubmit),
validering og innsending må gjøres "manuelt" av foreldrekomponenten. Dette er ikke optimalt, men vi har vurdert det
som "godt nok".
</div>
Loading

0 comments on commit c0d3c5d

Please sign in to comment.