Repository: vigetlabs/microcosm
Pull request #521 (2018-06-26 14:40:56)

Add confirm prop to ActionButton and ActionForm
**What**

This PR adds a `confirm` prop to ActionButton and ActionForm that allows users to bail out of action dispatch if the result is false. Like:

```javascript
function askUser(value, event) {
  return confirm("Are you sure you want to do this?")
}

function ConfirmButton(props) {
  return (
    <ActionButton action={deleteItem} confirm={askUser}>
      Delete Item
    </ActionButton>
  )
}
```

**Why**

ActionButton doesn't have a great way to confirm actions like deleting a record. For symmetry, I also added this functionality to ActionForm.

---

**Note:** This is against 12.x, our stable release. I'll eventually move this over to 13.x too.

tommymarshall: Very cool!
codecov-io: # [Codecov](https://codecov.io/gh/vigetlabs/microcosm/pull/521?src=pr&el=h1) Report
> Merging [#521](https://codecov.io/gh/vigetlabs/microcosm/pull/521?src=pr&el=desc) into [12-stable](https://codecov.io/gh/vigetlabs/microcosm/commit/b5603fcac8601534a41aae024c276ec692ac80f2?src=pr&el=desc) will **increase** coverage by `<.01%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/vigetlabs/microcosm/pull/521/graphs/tree.svg?src=pr&token=AaE0FLz9sU&height=150&width=650)](https://codecov.io/gh/vigetlabs/microcosm/pull/521?src=pr&el=tree)

```diff
@@              Coverage Diff              @@
##           12-stable     #521      +/-   ##
=============================================
+ Coverage      99.78%   99.78%   +<.01%     
=============================================
  Files             26       26              
  Lines            936      944       +8     
=============================================
+ Hits             934      942       +8     
  Misses             2        2
```


| [Impacted Files](https://codecov.io/gh/vigetlabs/microcosm/pull/521?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/addons/action-button.js](https://codecov.io/gh/vigetlabs/microcosm/pull/521/diff?src=pr&el=tree#diff-c3JjL2FkZG9ucy9hY3Rpb24tYnV0dG9uLmpz) | `100% <100%> (ø)` | :arrow_up: |
| [src/addons/action-form.js](https://codecov.io/gh/vigetlabs/microcosm/pull/521/diff?src=pr&el=tree#diff-c3JjL2FkZG9ucy9hY3Rpb24tZm9ybS5qcw==) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/vigetlabs/microcosm/pull/521?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/vigetlabs/microcosm/pull/521?src=pr&el=footer). Last update [b5603fc...f6b8210](https://codecov.io/gh/vigetlabs/microcosm/pull/521?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/api/action-button.md b/docs/api/action-button.md
index a6e2f533..f4f87a66 100644
--- a/docs/api/action-button.md
+++ b/docs/api/action-button.md
@@ -82,6 +82,29 @@ Called after the button is clicked but before the action is broadcasted.
 This provides an opportunity to alter the action payload (e.g. based on
 the event object) before it gets dispatched.
 
+### confirm(value, event)
+
+Called right before an ActionButton will dispatch an action. Return `true` or `false` from this callback in order to control whether or not the ActionButton dispatches. This is useful for:
+
+- Asking a user if they want to perform an action
+- Only applying a behavior for specific keyboard input (like if the control key is pressed)
+
+`value` is the result of calling the `prepare` property.
+
+```javascript
+function askUser(value, event) {
+  return confirm('Are you sure you want to do this?')
+}
+
+function ConfirmButton(props) {
+  return (
+    <ActionButton action={deleteItem} confirm={askUser}>
+      Delete Item
+    </ActionButton>
+  )
+}
+```
+
 ### onClick(event, action)
 
 An event callback executed immediately after the button clicks and the
diff --git a/docs/api/action-form.md b/docs/api/action-form.md
index ca9b2e7a..4100854a 100644
--- a/docs/api/action-form.md
+++ b/docs/api/action-form.md
@@ -107,6 +107,30 @@ class MyForm extends React.Component {
 }
 ```
 
+### confirm(value, event)
+
+Called right before an ActionForm will dispatch an action. Return `true` or `false` from this callback in order to control whether or not the ActionForm dispatches. This is useful for:
+
+- Asking a user if they want to perform an action
+- Only applying a behavior for specific keyboard input (like if the control key is pressed)
+
+`value` is the result of calling the `prepare` property.
+
+```javascript
+function askUser(value, event) {
+  return confirm('Are you sure you want to do this?')
+}
+
+function ConfirmButton({ user }) {
+  return (
+    <ActionForm action={update} confirm={askUser}>
+      <input name="email" type="email" defaultValue={user.email} />
+      <button>Submit</button>
+    </ActionForm>
+  )
+}
+```
+
 ### onSubmit(event, action)
 
 An event callback executed immediately after the form submits and the
diff --git a/package.json b/package.json
index 93d9d490..626fc427 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
   "scripts": {
     "build": "make all",
     "flow": "flow",
-    "lint": "prettier -l './{src,test,examples}/**/*.j' && eslint \"{src,test,examples}/**/*.js\"",
+    "lint": "prettier -l './{src,test,examples}/**/*.js' && eslint './{src,test,examples}/**/*.js'",
     "pretty": "prettier --write './{docs,src,test,examples}/**/*.{md,js,css}' && eslint --fix './{src,test,examples}/**/*.js'",
     "docs": "jsdoc src -c jsdoc/conf.json && jsdoc src -c jsdoc/conf.json --package package.json",
     "test": "jest --config test/dev.config.json --maxWorkers=2",
diff --git a/src/addons/action-button.js b/src/addons/action-button.js
index 7fbbfad0..ce1bc3d6 100644
--- a/src/addons/action-button.js
+++ b/src/addons/action-button.js
@@ -18,6 +18,7 @@ type Props = {
   onError: ?Callback,
   onOpen: ?Callback,
   onUpdate: ?Callback,
+  confirm: (value?: *, event?: Event) => boolean,
   prepare: (value?: *, event?: Event) => *,
   send: ?Sender,
   tag: string | React$ElementType,
@@ -33,7 +34,8 @@ class ActionButton extends React.PureComponent<Props> {
   static contextTypes: Context
 
   send: Sender
-  click: (event: Event) => Action
+  click: () => void
+  confirm: (event: Event) => void
 
   constructor(props: Props, context: Context) {
     super(props, context)
@@ -42,10 +44,14 @@ class ActionButton extends React.PureComponent<Props> {
     this.click = this.click.bind(this)
   }
 
-  click(event: Event) {
+  click(event) {
     let payload = this.props.prepare(this.props.value, event)
     let action = null
 
+    if (!this.props.confirm(payload, event)) {
+      return
+    }
+
     if (this.props.action) {
       action = this.send(this.props.action, payload)
 
@@ -77,6 +83,7 @@ class ActionButton extends React.PureComponent<Props> {
     delete props.onError
     delete props.send
     delete props.prepare
+    delete props.confirm
 
     if (this.props.tag === 'button' && props.type == null) {
       props.type = 'button'
@@ -99,6 +106,7 @@ ActionButton.defaultProps = {
   onOpen: null,
   onUpdate: null,
   prepare: identity,
+  confirm: (value, event) => true,
   send: null,
   tag: 'button',
   value: null
diff --git a/src/addons/action-form.js b/src/addons/action-form.js
index a8c70566..8321318e 100644
--- a/src/addons/action-form.js
+++ b/src/addons/action-form.js
@@ -20,7 +20,8 @@ type Props = {
   onCancel: ?Callback,
   onDone: ?Callback,
   onSubmit: (event: Event, Action: *) => *,
-  prepare: (data: Object) => Object,
+  confirm: (value?: *, event?: Event) => boolean,
+  prepare: (value?: *, event?: Event) => *,
   send: ?Sender,
   serializer: (form: Element) => Object
 }
@@ -61,6 +62,7 @@ class ActionForm extends React.PureComponent<Props> {
     // Remove invalid props to prevent React warnings
     delete props.action
     delete props.prepare
+    delete props.confirm
     delete props.serializer
     delete props.onOpen
     delete props.onDone
@@ -89,6 +91,10 @@ class ActionForm extends React.PureComponent<Props> {
     let params = this.props.prepare(this.props.serializer(form))
     let action = null
 
+    if (!this.props.confirm(params, event)) {
+      return
+    }
+
     if (this.props.action) {
       action = this.send(this.props.action, params)
 
@@ -119,6 +125,7 @@ ActionForm.defaultProps = {
   onDone: null,
   onSubmit: noop,
   prepare: identity,
+  confirm: (value, event) => true,
   send: null,
   serializer: form => serialize(form, { hash: true, empty: true })
 }


Test patch:
diff --git a/test/addons/action-button.test.js b/test/addons/action-button.test.js
index 2c2feeea..aa194e88 100644
--- a/test/addons/action-button.test.js
+++ b/test/addons/action-button.test.js
@@ -154,6 +154,42 @@ describe('callbacks', function() {
   })
 })
 
+describe('confirm', () => {
+  it('will not dispatch an action if confirm returns false', function() {
+    let send = jest.fn()
+
+    let wrapper = mount(<ActionButton confirm={() => false} />, {
+      context: { send }
+    })
+
+    wrapper.simulate('click')
+
+    expect(send).not.toHaveBeenCalled()
+  })
+
+  it('confirm receives the result of prepare', function() {
+    let send = jest.fn()
+    let confirm = jest.fn(payload => true)
+    let prepare = string => string.toUpperCase()
+
+    let wrapper = mount(
+      <ActionButton
+        action="action"
+        value="test"
+        prepare={prepare}
+        confirm={confirm}
+      />,
+      {
+        context: { send }
+      }
+    )
+
+    wrapper.simulate('click')
+
+    expect(confirm).toHaveBeenCalledWith('TEST', expect.anything())
+  })
+})
+
 describe('manual operation', function() {
   it('click can be called directly on the component instance', function() {
     let repo = new Microcosm()
diff --git a/test/addons/action-form.test.js b/test/addons/action-form.test.js
index 787c8451..f1f4c7b0 100644
--- a/test/addons/action-form.test.js
+++ b/test/addons/action-form.test.js
@@ -189,6 +189,37 @@ describe('serialization', function() {
   })
 })
 
+describe('confirmation', () => {
+  it('will not dispatch an action if confirm returns false', function() {
+    let send = jest.fn()
+
+    let wrapper = mount(<ActionForm action="test" confirm={() => false} />, {
+      context: { send }
+    })
+
+    wrapper.simulate('submit')
+
+    expect(send).not.toHaveBeenCalled()
+  })
+
+  it('confirm receives the result of prepare', function() {
+    let send = jest.fn()
+    let confirm = jest.fn(payload => true)
+    let prepare = () => 'TEST'
+
+    let wrapper = mount(
+      <ActionForm action="action" prepare={prepare} confirm={confirm} />,
+      {
+        context: { send }
+      }
+    )
+
+    wrapper.simulate('submit')
+
+    expect(confirm).toHaveBeenCalledWith('TEST', expect.anything())
+  })
+})
+
 describe('refs', function() {
   it('can be referenced as a ref', function() {
     let send = jest.fn()