Repository: vigetlabs/microcosm (js)

Issue:
microcosm-addons: manually pass payload to ActionForm
For controlled forms, it would be great if you could pass in the payload for the form to send:

```javascript
class ControlledForm extends React.Component {
  // ... controlled input stuff
  render() {
    return (
      <ActionForm action="thing" payload={this.state.formData}>
        <input type="text" onChange={this.onChange} />
      </ActionForm>
    )
  }
}
```

In this mode:

1. The `serializer` prop would **not** be called.
2. The `prepare` prop _would_ be called


Pull request description:
Add value prop to ActionForm
This commit adds a value prop to the ActionForm component of microcosm-dom. When set, the `value` prop is always passed as the serialized value of the form. This is intended to better support controlled forms.

This also achieves a nice parity with `ActionButton`, which lets you set a similar `value` property as the callback value when the button is clicked.

```javascript
import { login } from '../actions/session'

class Login extends React.Component {
  state = {
    email: '',
    password: ''
  }

  render() {
    let { email, password } = this.state

    return (
      <ActionForm action={login} value={this.state}>
        <input type="email" name="email" value={email} onChange={this.setField} />
        <input type="password" name="password" value={password} onChange={this.setField} />
      </ActionForm>
    )
  }

  setField = event => this.setState({ [event.target.name]: [event.target.value] })
}
```

Fixes #429

Patch:
diff --git a/packages/microcosm-dom/docs/action-form.md b/packages/microcosm-dom/docs/action-form.md
index 3d5b55eb..c6b1f0a0 100644
--- a/packages/microcosm-dom/docs/action-form.md
+++ b/packages/microcosm-dom/docs/action-form.md
@@ -1,8 +1,8 @@
 # ActionForm
 
-1. [Overview](#overview)
-2. [Usage](#usage)
-3. [Props](#props)
+1.  [Overview](#overview)
+2.  [Usage](#usage)
+3.  [Props](#props)
 
 ## Overview
 
@@ -11,26 +11,28 @@ ActionForm is a wrapper around a standard `form` tag that provides a method of b
 ```javascript
 import React from 'react'
 import DOM from 'react-dom'
-import Microcosm from 'microcosm'
+import { Microcosm, Domain } from 'microcosm'
 import { ActionForm, Presenter } from 'microcosm-dom'
 
 const repo = new Microcosm()
 
 const increaseCount = n => n
 
-repo.addDomain('count', {
+class Count extends Domain {
   getInitialState() {
     return 0
-  },
+  }
   increase(count, { amount }) {
     return count + amount
-  },
+  }
   register() {
     return {
       [increaseCount]: this.increase
     }
   }
-})
+}
+
+repo.addDomain('count', Count)
 
 function StepperForm({ count }) {
   return (
@@ -42,9 +44,9 @@ function StepperForm({ count }) {
 }
 
 class CountPresenter extends Presenter {
-  getModel() {
+  getModel(repo) {
     return {
-      count: state => state.count
+      count: repo.domains.count
     }
   }
 
@@ -67,6 +69,46 @@ ActionForm inputs are serialized to JSON upon submission using
 
 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.
 
+## value
+
+Optional. When provided, instead of serializing form inputs into an object, ActionForm will pass this value when it dispatches its action.
+
+```javascript
+import { login } from '../actions/session'
+
+class Login extends React.Component {
+  state = {
+    email: '',
+    password: ''
+  }
+
+  render() {
+    let { email, password } = this.state
+
+    return (
+      <ActionForm action={login} value={this.state}>
+        <input
+          type="email"
+          name="email"
+          value={email}
+          onChange={this.setField}
+        />
+        <input
+          type="password"
+          name="password"
+          value={password}
+          onChange={this.setField}
+        />
+      </ActionForm>
+    )
+  }
+
+  setField = event => {
+    this.setState({ [event.target.name]: [event.target.value] })
+  }
+}
+```
+
 ### 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.
diff --git a/packages/microcosm-dom/src/action-form.js b/packages/microcosm-dom/src/action-form.js
index 998b5442..c3907be9 100644
--- a/packages/microcosm-dom/src/action-form.js
+++ b/packages/microcosm-dom/src/action-form.js
@@ -36,15 +36,13 @@ export function generateActionForm(createElement, Component) {
       delete props.onCancel
       delete props.onError
       delete props.send
+      delete props.value
 
       return createElement(this.props.tag, props)
     }
 
     submit(event) {
-      const { prepare, serializer, action } = this.props
-
-      let params = prepare(serializer(this._form))
-      let result = this.send(action, params)
+      let result = this.send(this.props.action, this._parameterize())
 
       if (result && 'subscribe' in result) {
         this._onChange('start', result)
@@ -64,6 +62,14 @@ export function generateActionForm(createElement, Component) {
 
     // Private --------------------------------------------------- //
 
+    _parameterize() {
+      let { value, prepare, serializer } = this.props
+
+      return value !== undefined
+        ? prepare(value)
+        : prepare(serializer(this._form))
+    }
+
     _onChange(status, result) {
       this.props[toCallbackName(status)](result.payload, result.meta)
     }


Test patch:
diff --git a/packages/microcosm-dom/test/action-form.test.js b/packages/microcosm-dom/test/action-form.test.js
index bf9c1fd6..09499d49 100644
--- a/packages/microcosm-dom/test/action-form.test.js
+++ b/packages/microcosm-dom/test/action-form.test.js
@@ -157,4 +157,42 @@ describe('ActionForm', function() {
       expect(send).toHaveBeenCalled()
     })
   })
+
+  describe('value prop', function() {
+    it('sends a value prop when provided', function() {
+      let send = jest.fn()
+      let form = mount(<ActionForm action="test" send={send} value={true} />)
+
+      submit(form)
+
+      expect(send).toHaveBeenCalledWith('test', true)
+    })
+
+    it('sends null when the value prop is null', function() {
+      let send = jest.fn()
+      let form = mount(<ActionForm action="test" send={send} value={null} />)
+
+      submit(form)
+
+      expect(send).toHaveBeenCalledWith('test', null)
+    })
+
+    it('calls prepare on the value', function() {
+      let send = jest.fn()
+      let prepare = jest.fn(value => value.toUpperCase())
+      let form = mount(
+        <ActionForm
+          action="action"
+          send={send}
+          prepare={prepare}
+          value="test"
+        />
+      )
+
+      submit(form)
+
+      expect(prepare).toHaveBeenCalledWith('test')
+      expect(send).toHaveBeenCalledWith('action', 'TEST')
+    })
+  })
 })