-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add optional promise-aware behavior to frost-modal-form #125
Add optional promise-aware behavior to frost-modal-form #125
Conversation
### Overview This adds optional flags and behavior to the `frost-modal-form` component which allows it to behave in a convenient way when `onConfirm` returns a promise. #### Disabling the confirm button until `onConfirm` resolves When `onConfirm` triggers an action that is either expensive or not idempotent, we now can prevent spamming of the Confirm button by setting the `closeOnConfirm` to false, and `disableConfirmUntilOnConfirmResolves` to true. The text to be shown while disabled can be changed by overriding the `disabledConfirm` property. ``` {{frost-modal-form closeOnConfirm=false disableConfirmUntilOnConfirmResolves=true // optional override of button props: disabledConfirm=(object disabled=true isVisible= // (true) text= // ('Confirm') ) .... }} ``` #### Automatically close once `onConfirm` resolves ``` {{frost-modal-form closeOnConfirm=false closeAfterOnConfirmResolves=true .... }} ``` #### Demo updates The modal form demo has been expanded with an extra section (`/#/form-async`) demonstrating the new behavior. # Changelog - Add optional `closeAfterOnConfirmResolves` flag - Add optional `disableConfirmUntilOnConfirmResolves` flag - Allow forcing disabled confirm button state until resolve - Show demo notifications above the blur layer - Add support for title tooltips on modal buttons - Update to ember-test-utils@^8.1.0 - Add eslint rules to allow existing test names - Fix linting per updated linting rules - Add chai-jquery, sinon-chai devDependencies
Changes Unknown when pulling 603af8f on gknoy:gknoy/add-promise-aware-confirmation into ** on ciena-frost:master**. |
addon/components/dialogs/form.js
Outdated
params () { | ||
@computed('forceDisabledConfirm') | ||
params (forceDisabledConfirm) { | ||
// We look these up here, rather than using arguments to computed(), so that we avoid double-computed errors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All you have done is prevent a log message from appearing - you have not stopped double renders from occurring or the work associated with the renders. The objective should not be to avoid the error message - it should be to avoid the scenario that causes the error to occur.
{{notification-message notification=notification}} | ||
{{/each}} | ||
</div> | ||
{{notification-container zindex=9999}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't zindex
need a -
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does in plain css
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't setting the style or CSS property, though, it's an argument to the Ember notification widget. The widget now sets its z-index explicitly with a style=
, and the way to override that is with a component property named zindex
.
This approach works, but I think adds unnecessarily to the interface. This behavior is probably what a developer will want more than 90% of the time, and there is currently no behavior when you hand a promise to the handler, so why don't we just make the disable-and-wait behavior default when given a promise? You can still do weird things by setting onConfirm to false and writing your own. |
I would like to discuss the approach that has been chosen to add the desired functionality. I'd much rather prefer to see a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be default behavior.
- Computes `confirmButtonProps` based on confirm + forceDisabledConfirm (currently only used by the form modal) - Updates `frost-modal-form` demos to show basic usage, promise-aware behavior, and extra options (the old demo, which had almost all the bells and whistles). - Tests of confirm button stuff use the test utils to check button state ;)
Changes Unknown when pulling 83d2f66 on gknoy:gknoy/add-promise-aware-confirmation into ** on ciena-frost:master**. |
I've updated the PR description to reflect the changes to the PR:
I think this still counts as a minor change, but it's conceivable that the new promise-aware behavior might break some expectations (specifically the disabling of the Confirm button). I suspect that most people that care about promise-aware interactions with a modal are already handling that themselves, though. Let me know if I need to edit the PR to be a Major -- or you can edit it to be so, I expect. :) |
text: PropTypes.string, | ||
title: PropTypes.string | ||
}), | ||
disabledConfirmText: PropTypes.string, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably go into the confirm namespace.
title: PropTypes.string | ||
}), | ||
disabledConfirmText: PropTypes.string, | ||
disableConfirmUntilOnConfirmResolves: PropTypes.bool, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this flag necessary? Can't they just not return a promise and figure out how to handle it in their own way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, if we need this - it should probably also go into the confirm namespace.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like your suggestion of putting the this and the alternate text in the confirm
property.
I still like your suggestion, but since we want this flag to default to true, I'd like consumers to be able to not need to specify it every time they override anything in config
. Per our discussion, I'll leave this as it is but move the text setting to the config
namespace. :)
@@ -31,6 +52,9 @@ const FrostModalBinding = Component.extend(PropTypesMixin, { | |||
noBlur: PropTypes.bool, | |||
targetOutlet: PropTypes.string, | |||
|
|||
// State | |||
forceDisabledConfirm: PropTypes.bool, // only used when we are currently overriding the confirm button state |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why this is part of the interface. It's late though... may just be my brain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
forceDisabledConfirm
was not intended for the public interface, but because we want to initialize it in getDefaultProps()
, our convention is to declare the state variables in PropTypes as well. That's not really necessary, just a habit from the other apps I work with.
That said, it is also possible to fiddle that field ourself when we are opting to handle all of the onConfirm
/onClose
interactions ourself (by setting closeOnConfirm=false
). I'm not sure if that means we should declare it in the props interfaces, or leave that as a "private" flag which users can use at their own risk.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
Changes Unknown when pulling 81410c0 on gknoy:gknoy/add-promise-aware-confirmation into ** on ciena-frost:master**. |
.eslintrc.js
Outdated
@@ -2,5 +2,13 @@ module.exports = { | |||
extends: 'frost-standard', | |||
globals: { | |||
capture: false | |||
}, | |||
rules: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why were these added? Did you encounter any issues when you didn't have them? (Wouldn't have expected you too since you've upgraded ember-test-utils
to ^8.1.0
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
- Not sure on prroptypes, but complaining about Wrong Ones seems good
valid-test-descriptions
will fail ifit(...)
descriptions don't start with'should ...'
. This makes nearly every test in the repo fail linting. We can either grandfather them in temporarily, or permanently -- I didn't see much to gain from rewriting all of the test descriptions.- the no-multi-spaces one means you can put
//
two spaces in front of a comment, which is nice but not necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rules introduced by upgrading ember-test-utils
to ^8.1.0
are the latest rules we are following as an organization. As such your 2nd and 3rd bullet points need to be addressed, which means that the appropriate changes need to be made and not add additional repo-specific rules. For the first bullet point if this is not already being provided by ember-test-utils@^8.1.0
then an issue needs to be opened against it, as what is good for one repo is good for all repos.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests for almost everything in this repo are written in a non-idiomatic style; is it OK to defer fixing those to a follow-up PR? I'd like to have that be a separate PR both for legibility and because I had hoped to be able to use this feature this week. ;-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why you upgraded ember-test-utils
as part of this PR but since you did then we should do the work that is involved with its upgrade as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Besides, it's only Wednesday and the required changes shouldn't take more than 15-20 minutes tops ;-)
* @returns {Boolean} whether thing is a thenable | ||
*/ | ||
function isThenable (thing) { | ||
return (thing !== undefined) && (typeof thing.then === 'function') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be a check added in between these two for the existence of thing.then
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If thing
is not defined, then thing.then
will simply resolve as undefined
, and typeof that will still work:
> thing = {wat: 13}
{wat: 13}
> typeof(thing.then)
"undefined"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, right. Thanks :-)
@computed('confirm', 'forceDisabledConfirm', '_defaultConfirm') | ||
/** | ||
* Computed our Confirm button's props so that we can alter text/disabled in concord with onConfirm promises | ||
* TODO: Update confirm/error/info/warn to use this as well: they currently use this.confirm directly |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add an issue about this if you haven't already addressed this if. If you have please remove the comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added #127 to describe this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it time-intensive to just address these now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My initial impression when I first encountered the linting errors was that it was, but let me look further.
Javascript: 52 errors, 0 warnings
It's updating ~50 tests. Most of them should be text changes, so it's probably not so bad. I'll just update the test names, though, and not the style of expectations.
}) | ||
} | ||
|
||
confirmed = confirmed.then(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be in an else
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. The else
for the prior if
is to do nothing regarding the disabled state (and would therefore be empty).
This is merely the action we want to happen within the if closeOnConfirm ...
branch, namely call onClose in a promise-aware way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
confirmed = confirmed.then(() => {
this.onClose()
})
is always going to run with this block of code. The if
statement before it
if (this.get('disableConfirmUntilOnConfirmResolves')) {
this.set('forceDisabledConfirm', true)
confirmed = confirmed.then(() => {
this.set('forceDisabledConfirm', false)
})
}
may optionally set confirmed
to something as well.
So if the first if
statement is executed it doesn't matter what confirmed
was set to because the code setting confirmed
after the if
statement is always going to run.
Or am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because they're in the same block, you can re-assign promises. It's re-using the confirmed
variable.
I previously did
let thenable = confirmed
if (....) {
thenable = confirmed.then( /* fix button */ )
}
thenable = confirmed.then( /* invoke onClose */ )
return thenable
but then realized that thenable
was completely redundant, because one can do the same reassignment with confirmed
anyway.
The end result is returning a promise that is basically
confirmed
.then(() => {...}) // invoke onClose
.finally(() => { ... }) // fix button state
(or should be once I fix the bug I commented about in a sister comment :))
if (onConfirm) { | ||
onConfirm() | ||
confirmed = onConfirm() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if this fails? Line 165 or 170 isn't going to run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the onConfirm
promise fails, the dialog will not close, because of the way .then
chaining works. This is good (IMO) because it lets us keep the dialog open while an error notification (shown by onConfirm
) is displayed, allowing the user to fix their input.
For example, one might try to Create a New User, and get an error back form the API that the email address is already in use, or some other server-knowledge-only error
Good catch though about the state of the disabled button not working right, though. I don't want onClose
to be invoked, but I still want the confirm button to be re-enabled even if the promise is rejected. Can I do
confirmed
.finally(() => { ... }) // fix button state
.then(() => {...}) // invoke onClose
or do I need to move this block so that the final action (re-enabling confirmation) is done after the .onClose()
invocation so that I end up with
confirmed
.then(() => {...}) // invoke onClose
.finally(() => { ... }) // fix button state
?
/** | ||
* Handle confirmation in a promise-aware way. | ||
* If onConfirm returns a promise, closeOnConfirm will wait for it to resolve successfully before closing the modal. | ||
* @returns {Object} the return value of onConfirm. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this.get('closeOnConfirm') !== true
this is not what gets returned
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I think since there's no way to normally get the return value directly (it's just bound to the click handler), I had overlooked that. Thanks for pointing that out.
- frost-standard will already error when prop types are wrong, so we don't need to specify that - fix test descriptions now, rather than later
Changes Unknown when pulling 8e39a9d on gknoy:gknoy/add-promise-aware-confirmation into ** on ciena-frost:master**. |
} | ||
|
||
confirmed = confirmed.catch((err) => { | ||
deps.Logger.log(err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we not want it in the log as an error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do, thanks for catching that! I don't think I have ever used Logger.error
before, so I forgot it existed.
Changes Unknown when pulling ef040dd on gknoy:gknoy/add-promise-aware-confirmation into ** on ciena-frost:master**. |
Changes Unknown when pulling 5f196bc on gknoy:gknoy/add-promise-aware-confirmation into ** on ciena-frost:master**. |
Changes Unknown when pulling 5f196bc on gknoy:gknoy/add-promise-aware-confirmation into ** on ciena-frost:master**. |
This project uses semver, please check the scope of this pr:
Overview
This PR adds optional flags and behavior to the
frost-modal-form
component which allows it to behave in a convenient way whenonConfirm
returns a Promise.Automatically close once
onConfirm
resolvesThe
closeOnConfirm
behavior has been upgraded to be Promise aware. IfonConfirm
returns a Promise, the modal will not callonClose
until the Promise resolves.Disables the confirm button until
onConfirm
resolvesWhen
onConfirm
returns a Promise, we now prevent spamming of the Confirm button by disabling it until the promise resolves. setting thecloseOnConfirm
to false, anddisableConfirmUntilOnConfirmResolves
to true.The text to be shown while disabled can be changed by overriding the
disabledConfirmText
property.If you do not want the Confirm button to be disabled while resolving the
onConfirm
return value, you can setdisableConfirmUntilOnConfirmResolves=false
:Screenshot
Demo updates
The modal form demo has been rewritten, so that there are samples of basic usage, promise-aware usage, and a demo of nearly all the bells and whistles (the original form demo route). Each of these demos integrate the Bunsen form's validation state, as one nearly always wants the Confirm button to be disabled until the form is valid.
Changelog
closeOnConfirm
to be promise-aware. IfonConfirm
returns a Promise, the modal will callonClose
once that Promise resolves. (Errors will leave the modal open.)disableConfirmUntilOnConfirmResolves
flag, and the ability to specify different text for a disabled confirm button. This behavior defaults totrue
, and is only active when handling anonConfirm
that returns a Promise.frost-modal-form
so that they show more realistic form modal usagetitle
tooltips on modal buttonschai-jquery
,sinon-chai
devDependencies^8.1.0
ofember-test-utils