Skip to content

Commit

Permalink
Update presenter docs, pass pure into presenter
Browse files Browse the repository at this point in the history
  • Loading branch information
nhunzaker committed Sep 8, 2016
1 parent 28f9981 commit 66e096f
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 24 deletions.
74 changes: 73 additions & 1 deletion docs/api/presenter.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
1. [Overview](#overview)
2. [Computed Properties](#computed-properties)
3. [Receiving Intents](#receiving-intents)
4. [API](#api)

## Overview

Expand Down Expand Up @@ -115,11 +116,12 @@ class CountPresenter extends Presenter {
return {
increaseCount: this.increaseCount
}
},
}

increaseCount(repo, { amount }) {
return repo.push(increment, amount)
}

render() {
return <StepperForm count={ this.state.count } />
}
Expand All @@ -136,3 +138,73 @@ invoke the method with the associated parameters.
If a Presenter does not implement an intent, it will bubble up to any
parent Presenters. If no Presenter implements the intent, an exception
will raise.

## API

### `presenterWillMount(repo, props)`

Similar to componentWillMount. Called before the presenter mounts.

### `presenterDidMount(repo, props)`

Similar to componentDidMount. Called when a presenter mounts.

### `presenterWillReceiveProps(repo, props)`

Similar to componentWillReceiveProps. Called when a presenter receives new props.

### `presenterWillUpdate(repo, props)`

Similar to componentWillUpdate. Called when a presenter is about to update.

### `presenterDidUpdate(repo, props)`

Similar to componentDidUpdate. Called when a presenter updates.

### `viewModel(props)`

Builds a view model for the current props. This must return an object of key/value
pairs. If the value is a function, it will be calculated by passing in the repo's
current state:

```javascript
class PlanetPresenter extends Presenter {
viewModel(props) {
return {
planet : state => state.planets.find(p => p.id === props.planetId)
}
}
// ...
}
```

If the Presenter is pure (passed in as either a prop or as an option to the
associated repo), this will only update state if shallowly equal.

### register()

Expose "intent" subscriptions to child components. This is used with the <Form />
add-on to improve the ergonomics of presenter/view communication (though this only
occurs from the view to the presenter).

```javascript
import Form from 'microcosm/addons/form'

class HelloWorldPresenter extends Presenter {
register() {
return {
'greet': this.greet
}
}
greet() {
alert("hello world!")
}
render() {
return (
<Form intent="greet">
<button>Greet</button>
</Form>
)
}
}
```
99 changes: 81 additions & 18 deletions src/addons/presenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,16 @@ export default class Presenter extends Component {
super(props, context)

this.repo = props.repo || context.repo
this.state = {}

this.updatePropMap(props)
if (this.repo) {
this.pure = props.hasOwnProperty('pure') ? props.pure : this.repo.pure
} else {
this.pure = !!props.pure
}

this._updatePropMap(props)

this.state = this._getState()
}

getChildContext () {
Expand All @@ -26,9 +33,7 @@ export default class Presenter extends Component {
}

/**
* Called before a presenter will mount it's view (returned from the
* render method). This hook provides a way to "prepare" work before
* rendering to the page.
* Proxy to componentWillMount
*
* @param {Microcosm} repo - The presenter's Microcosm instance
* @param {Object} props - The presenter's props
Expand All @@ -37,6 +42,16 @@ export default class Presenter extends Component {
// NOOP
}

/**
* Proxy to componentDidMount.
*
* @param {Microcosm} repo - The presenter's Microcosm instance
* @param {Object} props - The presenter's props
*/
presenterDidMount (repo, props) {
// NOOP
}

/**
* Called when a presenter receives new props. This hook provides a way to
* perform new work if a presenter is given new props. For example: if a
Expand All @@ -49,29 +64,69 @@ export default class Presenter extends Component {
// NOOP
}

/**
* Proxy to componentWillUpdate.
*
* @param {Microcosm} repo - The presenter's Microcosm instance
* @param {Object} props - The presenter's props
*/
presenterWillUpdate (repo, props) {
// NOOP
}

/**
* Proxy to componentWillUnmount.
*
* @param {Microcosm} repo - The presenter's Microcosm instance
* @param {Object} props - The presenter's props
*/
presenterWillUnmount (repo, props) {
// NOOP
}

/**
* Proxy to componentWillUpdate.
*
* @param {Microcosm} repo - The presenter's Microcosm instance
* @param {Object} props - The presenter's props
*/
presenterDidUpdate (repo, props) {
// NOOP
}

componentWillMount () {
this.presenterWillMount(this.repo, this.props)
this.updateState()
}

componentDidMount () {
this._listener = this.updateState.bind(this)
this._listener = this._updateState.bind(this)

this.repo.on('change', this._listener, true)

this.presenterDidMount(this.repo, this.props)
}

componentWillUpdate (props) {
this.presenterWillUpdate(this.repo, props)
}

componentDidUpdate (props) {
this.presenterDidUpdate(this.repo, props)
}

componentWillUnmount () {
this.repo.off('change', this._listener, true)
this.presenterWillUnmount(this.repo, this.props)
}

componentWillReceiveProps (nextProps) {
if (this.props.pure === false || shallowEqual(nextProps, this.props) === false) {
this.updatePropMap(nextProps)
this._updatePropMap(nextProps)
}

this.presenterWillReceiveProps(this.repo, nextProps)

this.updateState()
this._updateState()
}

/**
Expand All @@ -91,23 +146,32 @@ export default class Presenter extends Component {
* @param {Object} props - The presenter's props, or new props entering a presenter.
* @returns {Object} The properties to assign to state
*/
viewModel(props) {
viewModel (props) {
return {}
}

updatePropMap (props) {
/**
* @private
*/
_updatePropMap (props) {
this.propMap = this.viewModel(props)
}

updateState () {
const next = this.getState()
/**
* @private
*/
_updateState () {
const next = this._getState()

if (this.props.pure === false || shallowEqual(this.state, next) === false) {
return this.setState(next)
}
}

getState () {
/**
* @private
*/
_getState () {
const nextState = {}

for (let key in this.propMap) {
Expand All @@ -119,7 +183,7 @@ export default class Presenter extends Component {
return nextState
}

register() {
register () {
// NOOP
}

Expand All @@ -141,12 +205,11 @@ export default class Presenter extends Component {

if (registry && registry[intent]) {
return registry[intent].apply(this, [ this.repo, ...params ])
}
else if (this.context.send) {
} else if (this.context.send) {
return this.context.send(intent, ...params)
}

throw new Error(`No presenter implements intent “${ intent }”.`)
console.warn(`No presenter implements intent “${ intent }”.`)
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/microcosm.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default class Microcosm extends Emitter {
/**
* @param {{maxHistory: Number}} options - Instantiation options.
*/
constructor ({ maxHistory = -Infinity, pure = false } = {}) {
constructor ({ maxHistory = -Infinity, pure = true } = {}) {
super()

this.history = new Tree()
Expand Down
13 changes: 9 additions & 4 deletions test/addons/presenter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import test from 'ava'
import React from 'react'
import Microcosm from '../../src/microcosm'
import Presenter from '../../src/addons/presenter'
import console from '../helpers/console'
import {mount} from 'enzyme'

const View = React.createClass({
Expand Down Expand Up @@ -36,16 +37,20 @@ test('receives intent events', t => {
mount(<MyPresenter repo={ new Microcosm() } />).find(View).simulate('click')
})

test('throws if no presenter implements an intent', t => {
test('warns if no presenter implements an intent', t => {
class MyPresenter extends Presenter {
render() {
return <View />
}
}

t.throws(function() {
mount(<MyPresenter repo={ new Microcosm() } />).find(View).simulate('click')
}, /implements intent/)
console.record()

mount(<MyPresenter repo={ new Microcosm() } />).find(View).simulate('click')

t.is(console.count('warn'), 1)

console.restore()
})

test('builds the view model into state', t => {
Expand Down

0 comments on commit 66e096f

Please sign in to comment.