Repository: vigetlabs/microcosm
Pull request #261 (2017-03-16 15:12:44)

Fix patch domains
Available for testing by installing `microcosm@12.2.1-alpha.0`

- adding domains does not reset state
- patching parents with keys owned by children does not patch children
- patch does not reset state in forks
- new children props re-render presenters
- presenter.send method is autobound, allowing it work when passed to children
---

Related issues:
https://github.com/vigetlabs/microcosm/issues/260
https://github.com/vigetlabs/microcosm/issues/259


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b8d6b04..a52e932c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
 # Changelog
 
+## 12.2.1 (alpha)
+
+- adding domains does not reset state
+- patching parents with keys owned by children does not patch children
+- patch does not reset state in forks
+- new children props re-render presenters
+- presenter.send method is autobound, allowing it work when passed to children
+
 ## 12.2.0
 
 - Added method for removing actions from history, history `append`
diff --git a/package.json b/package.json
index 67c06b92..547b7033 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "microcosm",
-  "version": "12.2.0",
+  "version": "12.2.1-alpha.0",
   "private": true,
   "description": "Flux with actions at center stage. Write optimistic updates, cancel requests, and track changes with ease.",
   "main": "microcosm.js",
diff --git a/src/addons/presenter.js b/src/addons/presenter.js
index cd68bead..8994e091 100644
--- a/src/addons/presenter.js
+++ b/src/addons/presenter.js
@@ -16,6 +16,9 @@ function Presenter (props, context) {
   } else {
     this.defaultRender = passChildren
   }
+
+  // Autobind send so that context is maintained when passing send to children
+  this.send = this.send.bind(this)
 }
 
 inherit(Presenter, PureComponent, {
@@ -89,7 +92,8 @@ inherit(Presenter, PureComponent, {
     return (
       createElement(PresenterMediator, {
         presenter   : this,
-        parentState : this.state
+        parentState : this.state,
+        parentProps : this.props
       })
     )
   }
diff --git a/src/lifecycle.js b/src/lifecycle.js
index f3f427ae..82805eba 100644
--- a/src/lifecycle.js
+++ b/src/lifecycle.js
@@ -12,7 +12,12 @@ function sandbox (data, deserialize) {
       }
     }
 
-    action.resolve(payload)
+    // Strip out keys not managed by this repo. This prevents
+    // children from accidentally having their keys reset by
+    // parents.
+    let sanitary = repo.realm.prune({}, payload)
+
+    action.resolve(sanitary)
   }
 }
 
@@ -31,3 +36,7 @@ export const BIRTH = function $birth () {
 export const START = function $start () {
   console.assert(false, 'Start lifecycle method should never be invoked directly.')
 }
+
+export const ADD_DOMAIN = function $addDomain (domain) {
+  return domain
+}
diff --git a/src/microcosm.js b/src/microcosm.js
index e8a4ac68..3de1585a 100644
--- a/src/microcosm.js
+++ b/src/microcosm.js
@@ -11,7 +11,8 @@ import getRegistration from './get-registration'
 import {
   RESET,
   PATCH,
-  BIRTH
+  BIRTH,
+  ADD_DOMAIN
 } from './lifecycle'
 
 import {
@@ -39,8 +40,6 @@ function Microcosm (options, state, deserialize)  {
   this.initial = this.parent ? this.parent.initial : {}
   this.state = this.parent ? this.parent.state : this.initial
 
-  this.follower = !!this.parent
-
   // Microcosm is now ready. Call the setup lifecycle method
   this.setup(options)
 
@@ -129,10 +128,6 @@ inherit(Microcosm, Emitter, {
   },
 
   reconcile (action) {
-    if (this.follower) {
-      return this
-    }
-
     let next = backfill(this.recall(action.parent), this.initial)
 
     if (this.parent) {
@@ -143,13 +138,17 @@ inherit(Microcosm, Emitter, {
       next = this.dispatch(next, action)
     }
 
+    if (this.parent) {
+      next = backfill(next, this.parent.recall(action))
+    }
+
     this.updateSnapshot(action, next)
 
     return this
   },
 
   prepareRelease () {
-    let next = this.follower ? this.parent.state : this.recall(this.history.head)
+    let next = this.recall(this.history.head)
 
     this.dirty = next !== this.state
     this.state = next
@@ -191,13 +190,11 @@ inherit(Microcosm, Emitter, {
   addDomain (key, config, options) {
     let domain = this.realm.add(key, config, options)
 
-    this.follower = false
-
     if (domain.getInitialState) {
       this.initial = set(this.initial, key, domain.getInitialState())
     }
 
-    this.history.checkout()
+    this.push(ADD_DOMAIN, domain)
 
     return domain
   },


Test patch:
diff --git a/test/addons/presenter.test.js b/test/addons/presenter.test.js
index 8bf7b5d5..ba35a1dd 100644
--- a/test/addons/presenter.test.js
+++ b/test/addons/presenter.test.js
@@ -1060,3 +1060,55 @@ describe('forks', function () {
   })
 
 })
+
+describe('::send', function () {
+
+  it('autobinds send', function () {
+    expect.assertions(2)
+
+    class Test extends Presenter {
+      prop = true
+
+      intercept () {
+        return {
+          test: () => {
+            expect(this).toBeInstanceOf(Test)
+            expect(this.prop).toBe(true)
+          }
+        }
+      }
+
+      view = function ({ send }) {
+        return <button onClick={() => send('test')}>Click me</button>
+      }
+    }
+
+    mount(<Test />).find('button').simulate('click')
+  })
+
+})
+
+describe('::children', function () {
+
+  it('re-renders when it gets new children', function () {
+    let wrapper = mount(<Presenter><span>1</span></Presenter>)
+
+    wrapper.setProps({ children: <span>2</span>})
+
+    expect(wrapper.text()).toEqual('2')
+  })
+
+  it('does not re-render when children are the same', function () {
+    let children = (<span>1</span>)
+    let wrapper = mount(<Presenter>{children}</Presenter>)
+
+    let presenter = wrapper.instance()
+
+    jest.spyOn(presenter, 'render')
+
+    wrapper.setProps({ children })
+
+    expect(presenter.render).not.toHaveBeenCalled()
+  })
+
+})
diff --git a/test/unit/microcosm/addDomain.test.js b/test/unit/microcosm/addDomain.test.js
index 30709969..fab1c68f 100644
--- a/test/unit/microcosm/addDomain.test.js
+++ b/test/unit/microcosm/addDomain.test.js
@@ -23,4 +23,58 @@ describe('Microcosm::addDomain', function () {
     expect(repo).toHaveState('b', 1)
   })
 
+  describe('forks', function () {
+    it('adding a domain to a fork does not reset all state', function () {
+      let repo = new Microcosm()
+
+      repo.addDomain('a', {
+        getInitialState () {
+          return 0
+        }
+      })
+
+      repo.reset({ a: 1 })
+
+      expect(repo).toHaveState('a', 1)
+
+      let fork = repo.fork()
+
+      fork.addDomain('b', {
+        getInitialState () {
+          return 2
+        }
+      })
+
+      expect(fork).toHaveState('a', 1)
+      expect(fork).toHaveState('b', 2)
+    })
+
+    it('adding a domain to a parent sends initial state to forks', function () {
+      let repo = new Microcosm()
+      let fork = repo.fork()
+
+      repo.addDomain('a', {
+        getInitialState () {
+          return 0
+        }
+      })
+
+      repo.reset({ a: 1 })
+
+      expect(repo).toHaveState('a', 1)
+      expect(fork).toHaveState('a', 1)
+
+      fork.addDomain('b', {
+        getInitialState () {
+          return 2
+        }
+      })
+
+      expect(repo).toHaveState('a', 1)
+      expect(fork).toHaveState('a', 1)
+
+      expect(repo).not.toHaveState('b')
+      expect(fork).toHaveState('b', 2)
+    })
+  })
 })
diff --git a/test/unit/microcosm/lifecycle.test.js b/test/unit/microcosm/lifecycle.test.js
index a45acaec..37b39e18 100644
--- a/test/unit/microcosm/lifecycle.test.js
+++ b/test/unit/microcosm/lifecycle.test.js
@@ -2,7 +2,8 @@ import Microcosm from '../../../src/microcosm'
 
 import {
   BIRTH,
-  START
+  START,
+  ADD_DOMAIN
 } from '../../../src/lifecycle'
 
 describe('Lifecycle', function () {
diff --git a/test/unit/microcosm/patch.test.js b/test/unit/microcosm/patch.test.js
index a4b890ba..97960a5a 100644
--- a/test/unit/microcosm/patch.test.js
+++ b/test/unit/microcosm/patch.test.js
@@ -98,6 +98,72 @@ describe('Microcosm::patch', function () {
       // the new state produced by a child.
       expect(child.state.count).toEqual(4)
     })
+
+    it('does not strip away parent inherited state', function () {
+      const parent = new Microcosm()
+      const child = parent.fork()
+
+      parent.addDomain('top', {
+        getInitialState() {
+          return false
+        }
+      })
+
+      child.addDomain('bottom', {
+        getInitialState() {
+          return false
+        }
+      })
+
+      child.patch({ bottom: true })
+
+      expect(child).toHaveState('top', false)
+      expect(child).toHaveState('bottom', true)
+    })
+
+    it('does not strip away parent inherited state when the parent patches', function () {
+      const parent = new Microcosm()
+      const child = parent.fork()
+
+      parent.addDomain('top', {
+        getInitialState() {
+          return false
+        }
+      })
+
+      child.addDomain('bottom', {
+        getInitialState() {
+          return false
+        }
+      })
+
+      parent.patch({ top: true })
+
+      expect(parent).toHaveState('top', true)
+      expect(child).toHaveState('top', true)
+    })
+
+    it('patching on a parent does not reset state of children', function () {
+      const parent = new Microcosm()
+      const child = parent.fork()
+
+      parent.addDomain('top', {
+        getInitialState() {
+          return false
+        }
+      })
+
+      child.addDomain('bottom', {
+        getInitialState() {
+          return false
+        }
+      })
+
+      parent.patch({ bottom: true })
+
+      expect(parent).not.toHaveState('bottom')
+      expect(child).toHaveState('bottom', false)
+    })
   })
 
 })