Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/mainmatter/ember-promise-modals

The easy solution for rendering and handling modals in Ember.js apps. Promised.
https://github.com/mainmatter/ember-promise-modals

emberjs emberjs-addon modals promises

Last synced: 3 days ago
JSON representation

The easy solution for rendering and handling modals in Ember.js apps. Promised.

Awesome Lists containing this project

README

        

# ember-promise-modals

The better way to handle modals in your Ember.js apps.

> [!NOTE]
> ember-promise-modals was written and is maintained by [Mainmatter](https://mainmatter.com) and contributors.
> We offer consulting, training, and team augmentation for Ember.js – check out our [website](https://mainmatter.com/ember-consulting/) to learn more!

## Compatibility

- Ember.js v3.28 or above
- Ember CLI v3.28 or above
- Node.js v16 or above

## Installation

```
ember install ember-promise-modals
```

## Usage

To use EPM in your project, add the target for the modals to your `application.hbs`:

```hbs

```

Then you can inject the `modals` [service](https://guides.emberjs.com/release/services/#toc_accessing-services) wherever you need and call its `open` method with a component class to render it as a modal.

```js
import { inject as service } from '@ember/service';
import ConfirmationModal from 'my-app/components/confirmation-modal';

export default class extends Component {
@service modals;

@action
async handleOpenModal() {
let modal = this.modals.open(ConfirmationModal);

// the instance acts as a promise that resolves with anything passed to the @close function
modal.then(results => {
// do something with the data
});

// so does `await`ing it!
let results = await modal;

// you can also close the modal from outside
modal.close();
}
}
```

```hbs

Click Me!

```

### Passing data to the rendered component

You can pass custom data into your rendered template like so:

```js
this.modals.open(FilePreviewModal, {
fileUrl: this.fileUrl,
});
```

All passed attributes can be accessed from the passed-in `data` object:

```hbs


```

```js
// components/file-preview.js
this.args.data.fileUrl; // or this.data.fileUrl in classic components
```

**NOTE:** By default, a `close` method is passed in your rendered component, in
order to trigger the "close modal" action. It can be called like so:

```hbs

Close
```

```js
// components/file-preview.js
this.args.close(results); // or this.close() in classic components
```

### Routable modals using the template helper

This addon comes with a `{{open-modal}}` template helper which can be used to trigger modals from any templates. It accepts the similar arguments as the `open` method. It can be used to open a modal in a route, closing it automatically when navigating elsewhere.

```hbs
{{open-modal this.ConfirmationModalComponent (hash fileUrl=this.fileUrl) close=(fn this.save results)}}
```

Positional arguments mimick the `open()` method on the service:

- `componentClass`: An imported component class of the modal to render
- `data`: Pass additional context to the modal,
- `options`: Pass additional options to the modal

Named arguments:

- `close` is called asynchronously with the data returned by the modals `@close` action when it is closed

## Animation

This addon uses CSS animations. You can either replace the
[styles of this addon](./addon/styles/ember-promise-modals.css) with your own
or adjust the defaults using CSS custom properties in your `:root{}`
declaration or in the CSS of any parent container of ``.

Available properties and their defaults can be found in the `:root {}` block inside the addons css.

By default, the animations are dropped when `prefers-reduced-motion` is
detected.

### Custom animations

To override the animation for a specific modal, an `options` object containing
a custom `className` can be handed to the `.open()` method.

```js
this.modals.open(
FilePreviwModal,
{
fileUrl: this.fileUrl,
},
{
// custom class, see below for example
className: 'custom-modal',
// optional: name the animation triggered by the custom css class
// animations ending in "-out" are detected by default!
// You most likely do not have to do this unless you absolutely
// can't have an animation ending in '-out'
animationKeyframesOutName: 'custom-animation-name-out',
// optional: a hook that is called when the closing animation of
// the modal (so not the backdrop) has finished.
onAnimationModalOutEnd: () => {},
},
);
```

```css
.custom-modal {
animation: custom-animation-in 0.5s;
opacity: 1;
transform: translate(0, 0);
}

/*
The `.epm-out` class is added to the parent of the modal when the modal
should be closed, which triggers the animation
*/
.custom-modal.epm-out {
animation: custom-animation-name-out 0.2s; /* default out animation is 2s */
opacity: 0;
transform: translate(0, 100%);
}

/*
animation name has to end in "-out" to be detected by the custom animationend
event handler
*/
@keyframes custom-animation-name-out {
0% {
opacity: 1;
transform: translate(0, 0);
}
100% {
opacity: 0;
transform: translate(0, 100%);
}
}
```

The CSS animations which are applied by the custom CSS class _must_ end in
`-out` to make the animations trigger the modal removal.

#### Examples

Examples for custom animations and how to apply them can be found in the addon's
dummy application.

See [the application.js controller](./packages/test-app/app/controllers/application.js)
for how the modals are opened in your JavaScript actions and look at
[app.css](./packages/test-app/app/styles/app.css) for the style definition of these
custom animations.

## Accessibility

User can press the Esc key to close the modal.

EPM uses [focus-trap](https://github.com/davidtheclark/focus-trap) internally
in order to handle user focus.

EPM will ensure to [focus the first "tabbable element" by default](https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_modal).
If no focusable element is present, focus will be applied on the currently
visible auto-generated container for the current modal.

Focus Trap can be configured both on the `modals` service, and the individual
modal level when calling `this.modals.open()`. Global and local options are used
in that order, which means that local config take precedence.

To set global Focus Trap config that all modals inherit, override the default
`Modals` service by extending it, place it to `app/services/modals.js`, then
use the `focusTrapOptions` property:

```js
import BaseModalsService from 'ember-promise-modals/services/modals';

export default class ModalsService extends BaseModalsService {
focusTrapOptions = {
clickOutsideDeactivates: false,
};
}
```

Example for local Focus Trap option, when opening a specific modal:

```js
this.modals.open(
FilePreviewModal,
{ fileUrl: this.fileUrl },
{
focusTrapOptions: {
clickOutsideDeactivates: false,
},
},
);
```

To disable Focus Trap completely, set `focusTrapOptions` to `null` on the
`modals` service:

```js
import BaseModalsService from 'ember-promise-modals/services/modals';

export default class ModalsService extends BaseModalsService {
focusTrapOptions = null;
}
```

Or when opening a modal:

```js
this.modals.open(
FilePreviewModal,
{ fileUrl: this.fileUrl },
{
focusTrapOptions: null,
},
);
```

⚠️ _We strongly advise against doing this. This will in most cases worsen the
accessibility of modals for your users. Be very careful._

## Testing

This addon provides a test helper function that reduces the timing for the CSS transitions to near zero to speed up your tests.

```js
import { setupPromiseModals } from 'ember-promise-modals/test-support';

module('Application | ...', function (hooks) {
// ...
setupPromiseModals(hooks);
// ...
});
```

## Migration guide

See the [Migration](MIGRATION.md) guide for details:

- From 0.x.x to 1.x.x about the replacement of ember-animated powered animations with CSS-based animations.
- From v4.x.x to v5.x.x about the removal of the PostCSS process.

## Contributing

See the [Contributing](CONTRIBUTING.md) guide for details.

## License

ember-promise-modals is developed by and © Mainmatter GmbH and contributors. It
is released under the [MIT License](LICENSE.md).