Repository: vigetlabs/microcosm
Pull request #431 (2017-10-03 11:59:26)

Pass form as second argument to action callbacks
Right now it's hard to reset a form when an action completes. This commit adds the form as the second argument of these callbacks, making this easier:

```javascript
class MyForm extends React.Component {
  onDone(_payload, form) { 
    form.reset()
  }

  render() {
    return (
      <ActionForm action="doSomething" onDone={this.onDone}>
        ...
      </ActionForm>
    )
  }
}
```

Fixes: https://github.com/vigetlabs/microcosm/issues/424

codecov-io: # [Codecov](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=h1) Report
> Merging [#431](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=desc) into [master](https://codecov.io/gh/vigetlabs/microcosm/commit/f908d4883f61626286d14ebe9f1f7add6ba076a3?src=pr&el=desc) will **decrease** coverage by `2.64%`.
> The diff coverage is `56.41%`.

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

```diff
@@            Coverage Diff             @@
##           master     #431      +/-   ##
==========================================
- Coverage   82.17%   79.52%   -2.65%     
==========================================
  Files          45       45              
  Lines        1335     1353      +18     
  Branches      243      246       +3     
==========================================
- Hits         1097     1076      -21     
- Misses        203      236      +33     
- Partials       35       41       +6
```


| [Impacted Files](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/microcosm/src/action.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtL3NyYy9hY3Rpb24uanM=) | `100% <ø> (+9.09%)` | :arrow_up: |
| [packages/microcosm/src/addons/action-form.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtL3NyYy9hZGRvbnMvYWN0aW9uLWZvcm0uanM=) | `0% <0%> (ø)` | :arrow_up: |
| [packages/microcosm/src/microcosm.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtL3NyYy9taWNyb2Nvc20uanM=) | `100% <100%> (ø)` | :arrow_up: |
| [packages/microcosm/src/coroutine.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtL3NyYy9jb3JvdXRpbmUuanM=) | `100% <100%> (ø)` | :arrow_up: |
| [packages/microcosm-preact/src/action-form.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtLXByZWFjdC9zcmMvYWN0aW9uLWZvcm0uanM=) | `91.89% <95.23%> (-8.11%)` | :arrow_down: |
| [packages/microcosm/src/addons/indexing.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtL3NyYy9hZGRvbnMvaW5kZXhpbmcuanM=) | `0% <0%> (-100%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/vigetlabs/microcosm/pull/431?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/431?src=pr&el=footer). Last update [f908d48...762dc1c](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codecov-io: # [Codecov](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=h1) Report
> Merging [#431](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=desc) into [master](https://codecov.io/gh/vigetlabs/microcosm/commit/f908d4883f61626286d14ebe9f1f7add6ba076a3?src=pr&el=desc) will **decrease** coverage by `3.29%`.
> The diff coverage is `94.44%`.

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

```diff
@@            Coverage Diff            @@
##           master     #431     +/-   ##
=========================================
- Coverage   82.17%   78.87%   -3.3%     
=========================================
  Files          45       45             
  Lines        1335     1349     +14     
  Branches      243      244      +1     
=========================================
- Hits         1097     1064     -33     
- Misses        203      237     +34     
- Partials       35       48     +13
```


| [Impacted Files](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/microcosm/src/microcosm.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtL3NyYy9taWNyb2Nvc20uanM=) | `100% <100%> (ø)` | :arrow_up: |
| [packages/microcosm/src/coroutine.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtL3NyYy9jb3JvdXRpbmUuanM=) | `42.85% <100%> (-57.15%)` | :arrow_down: |
| [packages/microcosm/src/addons/action-form.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtL3NyYy9hZGRvbnMvYWN0aW9uLWZvcm0uanM=) | `97.5% <93.33%> (+97.5%)` | :arrow_up: |
| [packages/microcosm-preact/src/action-form.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtLXByZWFjdC9zcmMvYWN0aW9uLWZvcm0uanM=) | `97.14% <94.73%> (-2.86%)` | :arrow_down: |
| [packages/microcosm/src/addons/model.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtL3NyYy9hZGRvbnMvbW9kZWwuanM=) | `0% <0%> (-100%)` | :arrow_down: |
| [packages/microcosm/src/action.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtL3NyYy9hY3Rpb24uanM=) | `100% <0%> (+9.09%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/vigetlabs/microcosm/pull/431?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/431?src=pr&el=footer). Last update [f908d48...444f443](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codecov-io: # [Codecov](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=h1) Report
> Merging [#431](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=desc) into [master](https://codecov.io/gh/vigetlabs/microcosm/commit/c6f88955481bb5cba9e1db562df26472ad6cc88c?src=pr&el=desc) will **decrease** coverage by `3.78%`.
> The diff coverage is `94.44%`.

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

```diff
@@            Coverage Diff             @@
##           master     #431      +/-   ##
==========================================
- Coverage   82.65%   78.87%   -3.79%     
==========================================
  Files          44       45       +1     
  Lines        1303     1349      +46     
  Branches      241      244       +3     
==========================================
- Hits         1077     1064      -13     
- Misses        191      237      +46     
- Partials       35       48      +13
```


| [Impacted Files](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/microcosm/src/microcosm.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtL3NyYy9taWNyb2Nvc20uanM=) | `100% <100%> (ø)` | :arrow_up: |
| [packages/microcosm/src/coroutine.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtL3NyYy9jb3JvdXRpbmUuanM=) | `42.85% <100%> (ø)` | :arrow_up: |
| [packages/microcosm/src/addons/action-form.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtL3NyYy9hZGRvbnMvYWN0aW9uLWZvcm0uanM=) | `97.5% <93.33%> (+97.5%)` | :arrow_up: |
| [packages/microcosm-preact/src/action-form.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtLXByZWFjdC9zcmMvYWN0aW9uLWZvcm0uanM=) | `97.14% <94.73%> (-2.86%)` | :arrow_down: |
| [packages/microcosm/src/addons/model.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtL3NyYy9hZGRvbnMvbW9kZWwuanM=) | `0% <0%> (-97.27%)` | :arrow_down: |
| [packages/microcosm-react-router/src/index.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtLXJlYWN0LXJvdXRlci9zcmMvaW5kZXguanM=) | `0% <0%> (ø)` | |
| [packages/microcosm/src/action.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtL3NyYy9hY3Rpb24uanM=) | `100% <0%> (+9.09%)` | :arrow_up: |
| [packages/microcosm/src/install-devtools.js](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=tree#diff-cGFja2FnZXMvbWljcm9jb3NtL3NyYy9pbnN0YWxsLWRldnRvb2xzLmpz) | `100% <0%> (+33.33%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/vigetlabs/microcosm/pull/431?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/431?src=pr&el=footer). Last update [c6f8895...4d1fb2d](https://codecov.io/gh/vigetlabs/microcosm/pull/431?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

efatsi: 👍 

Patch:
diff --git a/lib/uglify-name-cache.json b/lib/uglify-name-cache.json
index cc629941..b7aa2a4a 100644
--- a/lib/uglify-name-cache.json
+++ b/lib/uglify-name-cache.json
@@ -1,6 +1,6 @@
 {
   "props": {
-    "cname": 43,
+    "cname": 52,
     "props": {
       "$_updateModel": "a",
       "$_beginSetup": "b",
@@ -44,7 +44,16 @@
       "$_createSnapshot": "O",
       "$_updateSnapshot": "P",
       "$_rebase": "Q",
-      "$_removeSnapshot": "R"
+      "$_removeSnapshot": "R",
+      "$_onSubmit": "S",
+      "$_assignForm": "T",
+      "$_form": "U",
+      "$_onNext": "V",
+      "$_onOpen": "W",
+      "$_onUpdate": "X",
+      "$_onDone": "Y",
+      "$_onError": "Z",
+      "$_onCancel": "$"
     }
   }
 }
\ No newline at end of file
diff --git a/packages/microcosm-preact/CHANGELOG.md b/packages/microcosm-preact/CHANGELOG.md
index d75300bb..23a5289a 100644
--- a/packages/microcosm-preact/CHANGELOG.md
+++ b/packages/microcosm-preact/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## Edge
+
+- ActionForm action callbacks are given the related form as the second argument.
+
 ## 1.0.0
 
 - Update to conform to the Microcosm 12.x addon APIs.
diff --git a/packages/microcosm-preact/src/action-button.js b/packages/microcosm-preact/src/action-button.js
index bad23707..74b13be9 100644
--- a/packages/microcosm-preact/src/action-button.js
+++ b/packages/microcosm-preact/src/action-button.js
@@ -6,13 +6,14 @@ class ActionButton extends Component {
     super(props, context)
 
     this.send = this.props.send || this.context.send
+
     this.click = this.click.bind(this)
   }
 
   click(event) {
     let action = this.send(this.props.action, this.props.value)
 
-    if (action && action instanceof Action) {
+    if (action instanceof Action) {
       action.subscribe(this.props)
     }
 
diff --git a/packages/microcosm-preact/src/action-form.js b/packages/microcosm-preact/src/action-form.js
index 185a1f88..6f25e241 100644
--- a/packages/microcosm-preact/src/action-form.js
+++ b/packages/microcosm-preact/src/action-form.js
@@ -7,13 +7,17 @@ class ActionForm extends Component {
     super(props, context)
 
     this.send = this.props.send || this.context.send
-    this.onSubmit = this.onSubmit.bind(this)
+    this._onSubmit = this._onSubmit.bind(this)
+
+    this._assignForm = el => {
+      this._form = el
+    }
   }
 
   render() {
     let props = merge(this.props, {
-      ref: el => (this.form = el),
-      onSubmit: this.onSubmit
+      ref: this._assignForm,
+      onSubmit: this._onSubmit
     })
 
     // Remove invalid props to prevent React warnings
@@ -30,29 +34,82 @@ class ActionForm extends Component {
     return h('form', props)
   }
 
-  onSubmit(event) {
+  submit(event) {
+    const { onSubmit, prepare, serializer, action } = this.props
+
+    let form = this._form
+
+    console.assert(
+      form,
+      'ActionForm has no form reference and can not submit. This can happen',
+      'if submit() is called after the parent component has unmounted.'
+    )
+
+    if (action) {
+      let result = this.send(action, prepare(serializer(form)))
+
+      if (result instanceof Action) {
+        result.subscribe(
+          {
+            onNext: this._onNext,
+            onOpen: this._onOpen,
+            onUpdate: this._onUpdate,
+            onDone: this._onDone,
+            onError: this._onError,
+            onCancel: this._onCancel
+          },
+          this
+        )
+      }
+    }
+
+    onSubmit(event, action)
+  }
+
+  /* Private ------------------------------------------------------ */
+
+  _onSubmit(event) {
     event.preventDefault()
     this.submit(event)
   }
 
-  submit(event) {
-    let form = this.form
-    let params = this.props.prepare(this.props.serializer(form))
-    let action = this.send(this.props.action, params)
+  _onNext(action) {
+    this.props.onNext(action, this._form)
+  }
 
-    if (action && action instanceof Action) {
-      action.subscribe(this.props)
-    }
+  _onOpen(payload) {
+    this.props.onOpen(payload, this._form)
+  }
+
+  _onUpdate(payload) {
+    this.props.onUpdate(payload, this._form)
+  }
 
-    this.props.onSubmit(event, action)
+  _onError(payload) {
+    this.props.onError(payload, this._form)
+  }
+
+  _onDone(payload) {
+    this.props.onDone(payload, this._form)
+  }
+
+  _onCancel(payload) {
+    this.props.onCancel(payload, this._form)
   }
 }
 
+const identity = n => n
+
 ActionForm.defaultProps = {
-  action: null,
-  serializer: form => serialize(form, { hash: true, empty: true }),
-  prepare: n => n,
-  onSubmit: n => n
+  onCancel: identity,
+  onDone: identity,
+  onError: identity,
+  onNext: identity,
+  onOpen: identity,
+  onSubmit: identity,
+  onUpdate: identity,
+  prepare: identity,
+  serializer: form => serialize(form, { hash: true, empty: true })
 }
 
 export default ActionForm
diff --git a/packages/microcosm/CHANGELOG.md b/packages/microcosm/CHANGELOG.md
index 91028f9c..92959286 100644
--- a/packages/microcosm/CHANGELOG.md
+++ b/packages/microcosm/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog 
 
+## Edge
+
+- ActionForm action callbacks are given the related form as the second argument.
+
 ## 12.13.0
 
 - In batching mode, Presenter model updates are asynchronous. 
diff --git a/packages/microcosm/docs/api/action-form.md b/packages/microcosm/docs/api/action-form.md
index d03dc37f..80bd47aa 100644
--- a/packages/microcosm/docs/api/action-form.md
+++ b/packages/microcosm/docs/api/action-form.md
@@ -6,11 +6,7 @@
 
 ## Overview
 
-ActionForm is a wrapper around a standard `form` tag that provides a method of
-broadcasting an action to associated Presenters (see
-[`./presenter.md`](./presenter.md)). This attempts to more closely model the
-way forms traditionally work, however within the context of a JavaScript
-application.
+ActionForm is a wrapper around a standard `form` tag that provides a method of broadcasting an action to associated Presenters (see [`./presenter.md`](./presenter.md)). This attempts to more closely model the way forms traditionally work, however within the context of a JavaScript application.
 
 ```javascript
 import React from 'react'
@@ -47,13 +43,15 @@ function StepperForm ({ count }) {
 }
 
 class CountPresenter extends Presenter {
-  model () {
+  getModel () {
     return {
       count : state => state.count
     }
   }
 
-  view ({ count }) {
+  render () {
+    const { count } = this.model
+
     return <StepperForm count={ count } />
   }
 }
@@ -68,21 +66,15 @@ ActionForm inputs are serialized to JSON upon submission using
 
 ### action
 
-A string value to send to Presenters. If a Presenter is registered to
-that string via its `intercept()` method, it will execute the
-associated callback.
+A string value to send to Presenters. If a Presenter is registered to that string via its `intercept()` method, it will execute the associated callback.
 
 ### serializer(form)
 
-The serialization function. By default this uses
-[`form-serialize`](https://github.com/defunctzombie/form-serialize). On
-submission, this function is given the associated form HTML element.
+The serialization function. By default this uses [`form-serialize`](https://github.com/defunctzombie/form-serialize). On submission, this function is given the associated form HTML element.
 
 ### prepare(params)
 
-Executed after serialization to allow for extra parameter
-manipulation. This is useful for ensuring proper date formats, or
-other data formats that may come directly from a form input.
+Executed after serialization to allow for extra parameter manipulation. This is useful for ensuring proper date formats, or other data formats that may come directly from a form input.
 
 ```javascript
 class MyForm extends React.Component {
@@ -109,31 +101,24 @@ class MyForm extends React.Component {
 
 ### onSubmit(event, action)
 
-An event callback executed immediately after the form submits and the
-action is broadcasted.
+An event callback executed immediately after the form submits and the action is broadcasted.
 
-### onOpen(payload)
+### onOpen(payload, form)
 
-After broadcasting, if the dispatched action returns a Microcosm
-action, this callback will execute when the action opens.
+After broadcasting, if the dispatched action returns a Microcosm action, this callback will execute when the action opens.
 
-### onDone(payload)
+### onDone(payload, form)
 
-After broadcasting, if the dispatched action returns a Microcosm
-action, this callback will execute if the action completes successfully.
+After broadcasting, if the dispatched action returns a Microcosm action, this callback will execute if the action completes successfully.
 
-### onError(payload)
+### onError(payload, form)
 
-After broadcasting, if the dispatched action returns a Microcosm
-action, this callback will execute if the action is rejected.
+After broadcasting, if the dispatched action returns a Microcosm action, this callback will execute if the action is rejected.
 
-### onCancel(payload)
+### onCancel(payload, form)
 
-After broadcasting, if the dispatched action returns a Microcosm
-action, this callback will execute when the action is cancelled.
+After broadcasting, if the dispatched action returns a Microcosm action, this callback will execute when the action is cancelled.
 
-### onUpdate(payload)
+### onUpdate(payload, form)
 
-After broadcasting, if the dispatched action returns a Microcosm
-action, this callback will execute when the action emits a progress
-update.
+After broadcasting, if the dispatched action returns a Microcosm action, this callback will execute when the action emits a progress update.
diff --git a/packages/microcosm/flow/action-form.js.flow b/packages/microcosm/flow/action-form.js.flow
new file mode 100644
index 00000000..269da02c
--- /dev/null
+++ b/packages/microcosm/flow/action-form.js.flow
@@ -0,0 +1,11 @@
+/**
+ * @flow
+ */
+
+type ActionFormCallback = (payload?: *, form?: HTMLFormElement) => void
+
+type ActionFormSerializer = (form: HTMLFormElement) => Object
+
+type ActionFormPreparer = (data: Object) => Object
+
+type ActionFormSubmit = (event?: Event, Action?: *) => void
diff --git a/packages/microcosm/flow/presenter.js.flow b/packages/microcosm/flow/presenter.js.flow
index fb2c2bf2..62b6b3c3 100644
--- a/packages/microcosm/flow/presenter.js.flow
+++ b/packages/microcosm/flow/presenter.js.flow
@@ -2,4 +2,6 @@
  * @flow
  */
 
-declare type Sender = (intent: Command | Tagged, ...params: *[]) => *
+declare type Intent = string | Command
+
+declare type Sender = (intent: Intent, ...params: *[]) => *
diff --git a/packages/microcosm/src/addons/action-form.js b/packages/microcosm/src/addons/action-form.js
index fb93c100..5f87edbb 100644
--- a/packages/microcosm/src/addons/action-form.js
+++ b/packages/microcosm/src/addons/action-form.js
@@ -6,20 +6,18 @@ import React from 'react'
 import { Action, merge } from '../index'
 import serialize from 'form-serialize'
 
-const identity = n => n
-
 type Props = {
-  action: *,
-  onOpen: ?Callback,
-  onUpdate: ?Callback,
-  onError: ?Callback,
-  onCancel: ?Callback,
-  onDone: ?Callback,
-  onNext: ?Callback,
-  onSubmit: (event: Event, Action: *) => *,
-  prepare: (data: Object) => Object,
-  send: ?Sender,
-  serializer: (form: Element) => Object
+  action?: string | Command,
+  onCancel: ActionFormCallback,
+  onDone: ActionFormCallback,
+  onError: ActionFormCallback,
+  onNext: ActionFormCallback,
+  onOpen: ActionFormCallback,
+  onSubmit: ActionFormSubmit,
+  onUpdate: ActionFormCallback,
+  prepare: ActionFormPreparer,
+  send?: Sender,
+  serializer: ActionFormSerializer
 }
 
 type Context = {
@@ -31,25 +29,26 @@ class ActionForm extends React.PureComponent<Props> {
   static contextTypes: Context
 
   send: Sender
-  form: Element
-  onSubmit: *
-  assignForm: Element => void
+
+  _form: HTMLFormElement
+  _assignForm: HTMLFormElement => void
+  _onSubmit: *
 
   constructor(props: Props, context: Context) {
     super(props, context)
 
     this.send = this.props.send || this.context.send
-    this.onSubmit = this.onSubmit.bind(this)
+    this._onSubmit = this._onSubmit.bind(this)
 
-    this.assignForm = el => {
-      this.form = el
+    this._assignForm = el => {
+      this._form = el
     }
   }
 
   render() {
     let props = merge(this.props, {
-      ref: this.assignForm,
-      onSubmit: this.onSubmit
+      ref: this._assignForm,
+      onSubmit: this._onSubmit
     })
 
     // Remove invalid props to prevent React warnings
@@ -67,15 +66,10 @@ class ActionForm extends React.PureComponent<Props> {
     return React.createElement('form', props)
   }
 
-  onSubmit(event: Event) {
-    event.preventDefault()
-    this.submit(event)
-  }
-
   submit(event: Event) {
     const { onSubmit, prepare, serializer, action } = this.props
 
-    let form = this.form
+    let form = this._form
 
     console.assert(
       form,
@@ -90,29 +84,70 @@ class ActionForm extends React.PureComponent<Props> {
       result = this.send(action, params)
 
       if (result && result instanceof Action) {
-        result.subscribe(this.props)
+        result.subscribe(
+          {
+            onNext: this._onNext,
+            onOpen: this._onOpen,
+            onUpdate: this._onUpdate,
+            onDone: this._onDone,
+            onError: this._onError,
+            onCancel: this._onCancel
+          },
+          this
+        )
       }
     }
 
     onSubmit(event, action)
   }
+
+  /* Private ------------------------------------------------------ */
+
+  _onSubmit(event: Event) {
+    event.preventDefault()
+    this.submit(event)
+  }
+
+  _onNext(action: *) {
+    this.props.onNext(action, this._form)
+  }
+
+  _onOpen(payload: *) {
+    this.props.onOpen(payload, this._form)
+  }
+
+  _onUpdate(payload: *) {
+    this.props.onUpdate(payload, this._form)
+  }
+
+  _onError(payload: *) {
+    this.props.onError(payload, this._form)
+  }
+
+  _onDone(payload: *) {
+    this.props.onDone(payload, this._form)
+  }
+
+  _onCancel(payload: *) {
+    this.props.onCancel(payload, this._form)
+  }
 }
 
+const identity = (n: any) => n
+
 ActionForm.contextTypes = {
-  send: () => {}
+  send: () => null
 }
 
 ActionForm.defaultProps = {
-  action: null,
-  onOpen: null,
-  onUpdate: null,
-  onCancel: null,
-  onError: null,
-  onDone: null,
-  onNext: null,
+  onOpen: identity,
+  onUpdate: identity,
+  onCancel: identity,
+  onError: identity,
+  onDone: identity,
+  onNext: identity,
   onSubmit: identity,
   prepare: identity,
-  send: null,
   serializer: form => serialize(form, { hash: true, empty: true })
 }
 
diff --git a/packages/microcosm/src/coroutine.js b/packages/microcosm/src/coroutine.js
index 525cde5e..1f4f1123 100644
--- a/packages/microcosm/src/coroutine.js
+++ b/packages/microcosm/src/coroutine.js
@@ -53,13 +53,8 @@ function processGenerator(action: Action, body: GeneratorAction, repo: *) {
  * Coroutine is used by an action to determine how it should resolve
  * the body of their associated command.
  */
-export default function coroutine(
-  action: Action,
-  command: Command,
-  params: *[],
-  repo: any
-) {
-  let body = command.apply(null, params)
+export default function coroutine(action: Action, params: *[], repo: any) {
+  let body = action.command.apply(null, params)
 
   /**
    * Provide support for Promises:
diff --git a/packages/microcosm/src/microcosm.js b/packages/microcosm/src/microcosm.js
index 74316218..3765c548 100644
--- a/packages/microcosm/src/microcosm.js
+++ b/packages/microcosm/src/microcosm.js
@@ -249,7 +249,7 @@ class Microcosm extends Emitter implements Domain {
   push(command: any, ...params: *): Action {
     let action = this.append(command)
 
-    coroutine(action, action.command, params, this)
+    coroutine(action, params, this)
 
     console.assert(
       this.active,


Test patch:
diff --git a/packages/microcosm-preact/test/unit/action-form.test.js b/packages/microcosm-preact/test/unit/action-form.test.js
index df28a225..5dcd84ff 100644
--- a/packages/microcosm-preact/test/unit/action-form.test.js
+++ b/packages/microcosm-preact/test/unit/action-form.test.js
@@ -34,7 +34,7 @@ describe('callbacks', function() {
 
     form.dispatchEvent(new Event('submit'))
 
-    expect(onDone).toHaveBeenCalledWith(true)
+    expect(onDone).toHaveBeenCalledWith(true, form)
   })
 
   it('executes onError when that action completes', function() {
@@ -47,7 +47,7 @@ describe('callbacks', function() {
 
     form.dispatchEvent(new Event('submit'))
 
-    expect(onError).toHaveBeenCalledWith('bad')
+    expect(onError).toHaveBeenCalledWith('bad', form)
   })
 
   it('executes onOpen when that action opens', function() {
@@ -61,7 +61,7 @@ describe('callbacks', function() {
 
     action.open('open')
 
-    expect(onOpen).toHaveBeenCalledWith('open')
+    expect(onOpen).toHaveBeenCalledWith('open', form)
   })
 
   it('executes onUpdate when that action sends an update', function() {
@@ -77,10 +77,26 @@ describe('callbacks', function() {
 
     action.update('loading')
 
-    expect(onUpdate).toHaveBeenCalledWith('loading')
+    expect(onUpdate).toHaveBeenCalledWith('loading', form)
   })
 
-  it('does not execute onDone if not given an action', function() {
+  it('executes onCancel when that action sends an update', function() {
+    const onCancel = jest.fn()
+    const action = new Action(n => n)
+    const send = n => action
+
+    const form = mount(
+      <ActionForm action="test" onCancel={onCancel} send={send} />
+    )
+
+    form.dispatchEvent(new Event('submit'))
+
+    action.cancel('sorry')
+
+    expect(onCancel).toHaveBeenCalledWith('sorry', form)
+  })
+
+  it('does not execute onDone if send returns no action', function() {
     const onDone = jest.fn()
     const send = n => true
 
@@ -91,7 +107,7 @@ describe('callbacks', function() {
     expect(onDone).not.toHaveBeenCalled()
   })
 
-  it('does not execute onDone if not given an action', function() {
+  it('does not execute onDone if send returns no action', function() {
     const onError = jest.fn()
     const send = n => true
 
@@ -104,7 +120,7 @@ describe('callbacks', function() {
     expect(onError).not.toHaveBeenCalled()
   })
 
-  it('does not execute onUpdate if not given an action', function() {
+  it('does not execute onUpdate if send returns no action', function() {
     const onUpdate = jest.fn()
     const send = n => true
 
@@ -116,6 +132,16 @@ describe('callbacks', function() {
 
     expect(onUpdate).not.toHaveBeenCalled()
   })
+
+  it('does not push empty actions', function() {
+    const send = jest.fn()
+
+    const form = mount(<ActionForm send={send} />)
+
+    form.dispatchEvent(new Event('submit'))
+
+    expect(send).not.toHaveBeenCalled()
+  })
 })
 
 describe('manual operation', function() {
@@ -127,12 +153,12 @@ describe('manual operation', function() {
 
     form.dispatchEvent(new Event('submit'))
 
-    expect(onDone).toHaveBeenCalledWith(true)
+    expect(onDone).toHaveBeenCalledWith(true, form)
   })
 
   it('can pass in send manually', function() {
     const send = jest.fn()
-    const form = mount(<ActionForm send={send} />)
+    const form = mount(<ActionForm send={send} action="test" />)
 
     form.dispatchEvent(new Event('submit'))
 
diff --git a/packages/microcosm/test/addons/action-form.test.js b/packages/microcosm/test/addons/action-form.test.js
index 1fa56408..8608bb08 100644
--- a/packages/microcosm/test/addons/action-form.test.js
+++ b/packages/microcosm/test/addons/action-form.test.js
@@ -20,7 +20,7 @@ describe('callbacks', function() {
 
     form.simulate('submit')
 
-    expect(onDone).toHaveBeenCalledWith(true)
+    expect(onDone).toHaveBeenCalledWith(true, form.getDOMNode())
   })
 
   it('executes onError when that action completes', function() {
@@ -34,7 +34,7 @@ describe('callbacks', function() {
 
     form.simulate('submit')
 
-    expect(onError).toHaveBeenCalledWith('bad')
+    expect(onError).toHaveBeenCalledWith('bad', form.getDOMNode())
   })
 
   it('executes onOpen when that action opens', function() {
@@ -48,7 +48,7 @@ describe('callbacks', function() {
 
     form.simulate('submit')
 
-    expect(onOpen).toHaveBeenCalledWith('open')
+    expect(onOpen).toHaveBeenCalledWith('open', form.getDOMNode())
   })
 
   it('executes onUpdate when that action sends an update', function() {
@@ -65,7 +65,7 @@ describe('callbacks', function() {
 
     action.update('loading')
 
-    expect(onUpdate).toHaveBeenCalledWith('loading')
+    expect(onUpdate).toHaveBeenCalledWith('loading', form.getDOMNode())
   })
 
   it('does not execute callbacks if not given an action', function() {
@@ -106,7 +106,7 @@ describe('context', function() {
 
     form.simulate('submit')
 
-    expect(onDone).toHaveBeenCalledWith(true)
+    expect(onDone).toHaveBeenCalledWith(true, form.getDOMNode())
   })
 
   it('send as a prop overrides context', function() {
@@ -121,7 +121,7 @@ describe('context', function() {
 
     form.simulate('submit')
 
-    expect(onDone).toHaveBeenCalledWith('from-prop')
+    expect(onDone).toHaveBeenCalledWith('from-prop', form.getDOMNode())
   })
 })
 
@@ -137,7 +137,7 @@ describe('manual operation', function() {
 
     form.instance().submit()
 
-    expect(onDone).toHaveBeenCalledWith(true)
+    expect(onDone).toHaveBeenCalledWith(true, form.getDOMNode())
   })
 
   it('can pass in send manually', function() {