The better way to handle modals in your Ember.js apps.
- Ember.js v3.4 or above
- Ember CLI v3.4 or above
- Node.js v12, v14 or above
ember install ember-promise-modals
To use EPM in your project, add the target for the modals to your application.hbs
:
Then you can to inject the modals
service wherever you need and call its open
method with a component name to render it as a modal.
import { inject as service } from '@ember/service';
export default class extends Component {
@service modals;
@action
async handleOpenModal() {
let modal = this.modals.open('confirmation-modal');
// 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();
}
}
If your application uses Embroider, you need to pass the component class to the open
method instead of just the modals name. This will become the default behavior in the future.
import ConfirmationModal from '../components/confirmation-modal';
export default class extends Component {
//...
@action
handleOpenModal() {
this.modals.open(ConfirmationModal);
}
}
You can pass custom data into your rendered template like so:
this.modals.open('file-preview', {
fileUrl: this.fileUrl,
});
All passed attributes can be accessed from the passed-in data
object:
// 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:
// components/file-preview.js
this.args.close(results); // or this.close() in classic components
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.
Positional arguments mimick the open()
method on the service:
name
: The name of the modal component to renderdata
: 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
This addon uses CSS animations. You can either replace the
styles of this addon with your own
or adjust the defaults using CSS custom properties in your :root{}
declaration or in the CSS of any parent container of <EpmModalContainer />
.
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.
To override the animation for a specific modal, an options
object containing
a custom className
can be handed to the .open()
method.
this.modals.open(
'file-preview',
{
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: () => {},
},
);
.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 for custom animations and how to apply them can be found in the addons dummy application.
See the application.js controller for how the modals are openend in your JavaScript actions and look at app.css for the style definition of these custom animations.
The addons CSS is run through PostCSS by default, which will create static fallbacks for all custom properties using their defaults.
If your application uses PostCSS by itself, you can set excludeCSS
to true
inside your ember-cli-build.js
:
let app = new EmberAddon(defaults, {
// Add options here
'ember-promise-modals': {
excludeCSS: true,
},
});
Done that, you can use postcss-import
to import the uncompiled addon styles in your projects app/styles/app.css
:
@import 'ember-promise-modals';
User can press the Esc key to close the modal.
EPM uses focus-trap internally in order to handle user focus.
EPM will ensure to focus the first "tabbable element" by default. 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:
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:
this.modals.open(
'file-preview',
{ fileUrl: this.fileUrl },
{
focusTrapOptions: {
clickOutsideDeactivates: false,
},
},
);
To disable Focus Trap completely, set focusTrapOptions
to null
on the
modals
service:
import BaseModalsService from 'ember-promise-modals/services/modals';
export default class ModalsService extends BaseModalsService {
focusTrapOptions = null;
}
Or when opening a modal:
this.modals.open(
'file-preview',
{ fileUrl: this.fileUrl },
{
focusTrapOptions: null,
},
);
This addon provides a test helper function that reduces the timing for the CSS transitions to near zero to speed up your tests.
import { setupPromiseModals } from 'ember-promise-modals/test-support';
module('Application | ...', function (hooks) {
// ...
setupPromiseModals(hooks);
// ...
});
See the Migration guide for details.
See the Contributing guide for details.
This project is licensed under the MIT License.