ember-dialogs
v1.1.0
Published
Customizable implementation of `alert`, `confirm` and `prompt` modal dialogs with straightforward API
Downloads
148
Maintainers
Readme
ember-dialogs
Customizable implementation of alert
, confirm
and prompt
modal dialogs with straightforward API.
Modal dialogs are driven by liquid-tether.
Demo.
Features
- API is close to native
alert
,confirm
andprompt
. - Execute with JS or a simple template action. You no longer need to add a duplicate template entry every time you need a dialog.
- Use built-in looks or throw in your own.
- Can be effortlessly animated with
liquid-fire
. - Blocks page scrolling while a dialog is active.
- Prompt supports placeholder and initial value.
Planned features
You are welcome to contribute!
- Use namespaced names for helpers #1
- A11y/ARIA #2
- Keyboard support: tabindex, cancel with Esc, etc #3
- Autofocus on the input field #4
Why
Native alert
, confirm
and prompt
functions are very handy and simple to use, yet they have a few drawbacks:
- Their looks aren't customizable, always out of sync with your app's design.
- Their looks are different from browser to browser.
- There's no backdrop.
- They are a pain to test.
- :warning: They block the JS thread, messing with animations and mouse/finger events.
A common solution is to use a custom dialog. Every time you need one, you typically add a duplicate entry like this:
{{! before ember-dialogs }}
<button {{action (toggle 'isConfirmDialogOpen' this)}}>
Do the thing
</button>
{{#if isConfirmDialogOpen}}
{{confirm-dialog
message = 'Are you sure?'
actionOk = (action 'doTheThing')
actionCancel = (action (toggle 'isConfirmDialogOpen' this))
}}
{{/if}}
ember-dialogs
removes the redundancy, reducing the above boilerplate to:
{{! with ember-dialogs }}
<button {{action (confirm
message = 'Are you sure?'
actionOk = (action 'doTheThing')
actionCancel = (action (toggle 'isConfirmDialogOpen' this))
)}}>
Do the thing
</button>
Alternatively, you can trigger dialogs programmatically without having to update your templates for every invocation:
// Old-school, blocking approach
if (confirm('Are you sure?')) {
this._doTheThing()
}
// ember-dialogs non-blocking approach
this.get('dialogs').confirm({
message: 'Are you sure?',
actionOk () { this._doTheThing() },
})
Installation
ember i ember-dialogs
Then add this snippet to your application
template:
{{ember-dialogs}}
These components will no render anything until a dialog is invoked. They will be reused for every invocation.
Invoking a dialog from JS
Call alert
, confirm
or prompt
on the dialogs
service:
import Component from '@ember/component'
import { inject as service } from '@ember/service'
export default Component.extend({
dialogs: service(),
importantValue: false,
userDidReject: false,
actions: {
toggle (value) {
this.get('dialogs').confirm({
message: 'Are you sure?',
actionOk: () => { this.set('importantValue', value) }, // Use arrow function to keep the scope
actionCancel: () => { this.set('userDidReject', true) },
})
}
}
})
Alternatively, you can leverage the fact that ember-dialogs
methods return a promise:
toggle (value) {
this
.get('dialogs')
.confirm({message: 'Are you sure?'})
.then(() => { this.set('importantValue', value) })
.catch(() => { this.set('userDidReject', true) },
})
}
Note: these example use modern import paths available in newer Ember versions.
Invoking a dialog from a template
Use alert
, confirm
or prompt
helper:
import Component from '@ember/component'
import { inject as service } from '@ember/service'
export default Component.extend({
dialogs: service(),
importantValue: false,
})
<a href {{action (confirm
message = 'Are you sure?'
actionOk = (toggle 'importantValue' this)
)}}>
Toggle
</a>
Note: this example uses the toggle
helper from ember-composable-helpers.
Arguments
Both methods and helpers accept a single arguments with a hash of values:
| Argument | Type | Default value | Description |
|:--------------------|:---------------------------------|:--------------|:-------------------------------------------------------------------------------------------------------------|
| message
| String | | Text to appear in the dialog. |
| actionOk
| Closure action, function or null | null
| Action to execute when user presses OK. |
| actionCancel
| Closure action, function or null | null
| Action to execute when user presses Cancel (not applicable for alert
). |
| labelOk
| String | 'OK'
| Text to appear on the OK button. |
| labelCancel
| String | 'Cancel'
| Text to appear on the Cancel button (not applicable for alert
). |
| cancelVisible
| Boolean | true
| Whether to show the Cancel button (applicable only for prompt
). |
| value
| String | ''
| Initial value for the input of prompt
. |
| placeholder
| String | ''
| Placeholder for the input of prompt
. |
| backdrop
| Boolean | true
| Whether to render the backdrop. |
| backdropClickable
| Boolean | true
| Whether the backdrop triggers an action (actionOk
for alert
, actionCancel
for confirm
and prompt
. |
| blockScrolling
| Boolean | true
| Whether to block page scrolling while the dialog is active. |
Note: clicking backdrop triggers actionOk
for alert
and actionCancel
for confirm
/prompt
. actionCancel
will not be triggered in prompt
mode if cancelVisible
is disabled.
Animating
Use liquid-fire to apply transitions.
You can target the backdrop with this.hasClass('ember-dialogs-backdrop')
.
A dialog can be targeted with this.hasClass('ember-dialogs-dialogTether')
.
You can distinguish dialog type with this.toValue(str)
, values are 'alert'
, 'confirm'
and 'prompt'
.
In app/transitions.js
:
this.transition(
this.hasClass('ember-dialogs-backdrop'),
this.use('fade')
)
this.transition(
this.hasClass('ember-dialogs-dialog'),
this.use('toUp'),
this.reverse('toDown')
)
Customizing
In order to throw in your own HTML for the dialogs, use the block form of components in the application
template:
{{#ember-dialogs as |params|}}
<div class="dialog">
{{#if params.message}}
<p class="message">{{params.message}}</p>
{{/if}}
{{#if (eq params.type 'prompt')}}
{{input
class = 'input'
value = params.userInput
placeholder = params.placeholder
enter = (action params.actionOk params.userInput)
}}
{{/if}}
<button
class = "ok"
{{action params.actionOk params.userInput}}
>
{{params.labelOk}}
</button>
{{#if params.shouldDisplayCancel}}
<button
class = "cancel"
{{action params.actionCancel}}
>
{{params.labelCancel}}
</button>
{{/if}}
</div>
{{/ember-dialogs}}
Here's a list of params available as properties on the yielded object:
| Property | Expected type | Description |
|:----------------------|:---------------|:-------------------------------------------------------------------------------------------------------------------------------------|
| message
| String | Text to appear on the modal. |
| type
| String | 'alert'
, 'confirm'
or 'prompt'
|
| value
| String | Value for the prompt input field. Using it as {{input}}
'value will mutate the bound value as user types into the input. |
| userInput
| String | Same as value
but passed through reads
computed property. Mutating it does not update the bound value. |
| placeholder
| String | Placeholder for the prompt input field. |
| labelOk
| String | Label for the OK button. |
| labelCancel
| String | Label for the Cancel button. |
| actionOk
| Closure action | Action to execute when OK button is clicked. |
| actionCancel
| Closure action | Action to execute when Cancel button is clicked. |
| actionBackdrop
| Closure action | Action to execute when the backdrop is clicked: either OK or Cancel depending on dialog type. Useful for an 🗙 button in top corner. |
| backdropClickable
| Boolean | Whether the backdrop is clickable. Can be used to hide an 🗙 button in top corner. |
| cancelVisible
| Boolean | Value of the cancelVisible
property passed when calling a dialog. |
| shouldDisplayCancel
| Boolean | False for alert
, true for confirm
, equal to cancelVisible
for prompt
. |
Customizing the backdrop
The backdrop is a single HTML element which can be customized by applying CSS to .ember-dialogs-backdrop
.
Testing with page objects
ember-cli-page-object is the recommended way of accessing the dialogs in your tests
You can import pages objects like this:
import {backdrop, dialog} from '<your-app-name>/tests/pages/components/dialogs'
Here's a list of nodes on the page objects:
backdrop // The backdrop element
dialog // The dialog
dialog.message // The text label
dialog.input // The input field (only in prompt mode)
dialog.buttonOk // The confirmation button
dialog.buttonCancel // The cancellation button (only in confirm and prompt modes)
Every page object node has the following properties and methods:
.$ // the jQuery object of the element
.exists // whether the element exists
.index // index of the element among siblings
.visible // whether the element is not hidden with CSS
.text // trimmed text content of the element, including child elements
.attribute(str) // returns the value of the given attribute on element
.click() // trigger the click event on the element
.keyup(code) // trigger the `keyup` event on the element, use http://keycode.info/
.blur() // remove focus from the element
.focus() // set focus to the element
.hasClass(str) // returns true/false
// Applicable only to the input:
.value // value attr (applicable only to the input)
.placeholder // placeholder attr text (applicable only to the input)
.fill(str) // populate with text
Some usage examples:
assert.equal(alert.message.text, 'Are you sure?')
assert.ok(backdrop.hasClass('-ember-dialogs-clickable')
prompt.input.fill('I agree')
backdrop.click()
prompt.buttonOk.click()
assert.ok(confirm.$.offset().top > 100)
See our acceptance tests for some inspiration.
Nested usage
:warning: Nested usage is not recommented. Use promise chaining instead, and you won't have to worry about what's mentioned below.
If you invoke a dialog from another dialog's action, the inner dialog will not show up.
This is because the outer dialog will cleanup after itself and hide the inner dialog immediately after it shows up. The inner dialog doesn't even get to render.
To work around this issue, wrap the inner dialog invocation with next
. See demo app's nested
action.
Notable dependencies
Private API usage
This addon uses defineProperty
from '@ember/object'
which is marked as private in the API docs. If you know a better way to reset an overwritten reads
CP, please create an issue or PR.
License
MIT.
Credit
Build by Andrey Mihkaylov (lolmaus) and contributors.