Skip to content

Commit eb98426

Browse files
committed
- Added observable selectors
- Refactored tests a bit
1 parent e6ebf1b commit eb98426

11 files changed

+159
-18
lines changed

docs/API/createClass.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,11 @@ spec.actionCreators = { key: (args...) -> action, ... }
7575
Specify the action creators associated with this component class. Each property on the ```actionCreators``` object will be bound to each component instance and made available on the instance as a method.
7676
> **NB:** The redux-components core makes no assumptions about which middleware is present on a store. If you have e.g. `redux-thunk` middleware installed, you may of course return thunks from your actions and actionCreators.
7777
78-
### spec.actions
78+
### spec.actionDispatchers
7979
```coffeescript
80-
spec.actions = { key: (args...) -> action, ... }
80+
spec.actionDispatchers = { key: (args...) -> action, ... }
8181
```
82-
Specify actions associated with this component class. Actions are like action creators, except the value returned is automatically `dispatch()`ed to the Store where this component is mounted. The wrapped action function will return the same thing as `dispatch()`, allowing it to be used with thunks and other related middleware.
82+
Specify actions associated with this component class. Action dispatchers are like action creators, except the value returned is automatically `dispatch()`ed to the Store where this component is mounted. The wrapped action function will return the same thing as `dispatch()`, allowing it to be used with thunks and other related middleware.
8383

8484
### spec.selectors
8585
```coffeescript

src/DefaultMixin.coffee

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ scopeSelector = (sel, self) -> ->
88
sel.apply(self, fwdArgs)
99

1010
# Bind an action to automatically dispatch to the right store.
11-
bindAction = (actionCreator, self) -> ->
11+
dispatchAction = (actionCreator, self) -> ->
1212
self.store.dispatch(actionCreator.apply(self, arguments))
1313

1414
# DefaultMixin is mixed into all component specs automatically by createClass.
@@ -23,8 +23,8 @@ export default DefaultMixin = {
2323
if @actionCreators
2424
(@[acKey] = ac.bind(@)) for acKey, ac of @actionCreators
2525
# Bind actions
26-
if @actions
27-
(@[acKey] = bindAction(ac, @)) for acKey, ac of @actions
26+
if @actionDispatchers
27+
(@[acKey] = dispatchAction(ac, @)) for acKey, ac of @actionDispatchers
2828
# Scope selectors
2929
if @selectors
3030
(@[selKey] = scopeSelector(sel, @)) for selKey, sel of @selectors

src/ObservableSelectorMixin.coffee

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import makeSelectorObservable from './makeSelectorObservable'
2+
3+
export default ObservableSelectorMixin = {
4+
componentWillMount: ->
5+
if @selectors
6+
makeSelectorObservable(@, @[selKey]) for selKey of @selectors
7+
undefined
8+
}

src/index.coffee

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import createClass from './createClass'
33
import DefaultMixin from './DefaultMixin'
44
import mountComponent from './mountComponent'
55
import ReduxComponent from './ReduxComponent'
6+
import ObservableSelectorMixin from './ObservableSelectorMixin'
67
import { createComponent, SubtreeMixin } from './subtree'
78

89
export {
@@ -13,4 +14,5 @@ export {
1314
ReduxComponent
1415
createComponent
1516
SubtreeMixin
17+
ObservableSelectorMixin
1618
}

src/makeSelectorObservable.coffee

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { removeFromList, assign } from './util'
2+
import $$observable from 'symbol-observable'
3+
4+
# A quick and dirty "Subject" mixin.
5+
# XXX: Would like to depend on a trusted library here, but the only one I know of is rxJS which is simply too bloated.
6+
# zen-observable does not provide a Subject implementation.
7+
reentrantMutation = (self) ->
8+
# Reentrant mutation safety: make a copy of the observer list in case it is currently being
9+
# iterated.
10+
if not self.__nextObservers then self.__nextObservers = []
11+
if self.__nextObservers is self.__currentObservers
12+
self.__nextObservers = self.__currentObservers.slice()
13+
undefined
14+
15+
subjectMixin = {
16+
next: (x) ->
17+
observers = @__currentObservers = @__nextObservers
18+
if observers
19+
observer.next?(x) for observer in observers
20+
undefined
21+
22+
subscribe: (observer) ->
23+
reentrantMutation(@)
24+
@__nextObservers.push(observer)
25+
if @__nextObservers.length is 1 then @__isBeingObserved?(true)
26+
# Subscription object
27+
{
28+
unsubscribe: =>
29+
reentrantMutation(@)
30+
removeFromList(@__nextObservers, observer)
31+
if @__nextObservers.length is 0 then @__isBeingObserved?(false)
32+
undefined
33+
}
34+
}
35+
36+
# Make a selector on a ReduxComponentInstance into an ES7 Observable.
37+
export default makeSelectorObservable = (componentInstance, selector) ->
38+
# Make the selector a Subject.
39+
assign(selector, subjectMixin)
40+
41+
# Make the selector an ES7 Observable
42+
Object.defineProperty(selector, $$observable, { writable: true, configurable: true, value: (-> @) })
43+
44+
# Attach the selector to the Redux store when it is being observed.
45+
selector.__isBeingObserved = (isBeingObserved) ->
46+
if isBeingObserved
47+
lastSeenValue = undefined
48+
observeState = ->
49+
val = selector(componentInstance.store.getState())
50+
if val isnt lastSeenValue then lastSeenValue = val; selector.next(val)
51+
52+
observeState()
53+
@__unsubscriber = componentInstance.store.subscribe(observeState)
54+
else
55+
@__unsubscriber?(); delete @__unsubscriber
56+
undefined

src/util.coffee

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ export get = (object, path) ->
88
while object? and index < length
99
object = object[path[index++]]
1010
if (index is length) then object else undefined
11+
12+
export removeFromList = (list, value) ->
13+
if (i = list.indexOf(value)) > -1 then list.splice(i, 1)

test/01-basic.coffee

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
{ expect } = require 'chai'
2-
{ createStore } = require 'redux'
32

4-
createClass = require '../createClass'
5-
mountComponent = require '../mountComponent'
3+
{ createClass, mountComponent, createComponent, SubtreeMixin } = require '..'
4+
{ makeAStore } = require './helpers/store'
65

76
describe 'basic functions: ', ->
8-
makeAStore = -> createStore( (x) -> x )
97
store = makeAStore()
108

119
RootComponent = null

test/02-subtree.coffee

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
{ inspect } = require 'util'
22

33
{ expect, assert } = require 'chai'
4-
{ createStore } = require 'redux'
5-
6-
createClass = require '../createClass'
7-
mountComponent = require '../mountComponent'
8-
SubtreeMixin = require '../SubtreeMixin'
9-
createComponent = require '../createComponent'
4+
{ createClass, mountComponent, createComponent, SubtreeMixin } = require '..'
5+
{ makeAStore } = require './helpers/store'
106

117
describe 'subtree: ', ->
12-
makeAStore = -> createStore( (x) -> x )
138
store = makeAStore()
149

1510
RootComponent = null

test/03-reducer-indirection.coffee

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe 'reducer indirection: ', ->
3030
when @CHANGE_MAGIC_WORD then Object.assign({}, state, { magicWord: action.payload or 'please'})
3131
when currentState.magicWord then Object.assign({}, state, {payload: 'got the magic word'})
3232
else state
33-
actions: {
33+
actionDispatchers: {
3434
setValue: (val) -> { type: @SET, payload: val }
3535
sayMagicWord: -> { type: @state.magicWord }
3636
setMagicWord: (val) -> { type: @CHANGE_MAGIC_WORD, payload: val }
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{ inspect } = require 'util'
2+
3+
{ expect, assert } = require 'chai'
4+
{ createClass, mountComponent, createComponent, ObservableSelectorMixin } = require '..'
5+
{ makeAStore } = require './helpers/store'
6+
7+
expectTestSequence = (tests) ->
8+
i = 0
9+
{
10+
next: (x) -> expect(tests[i++]?(x)).to.equal(true)
11+
error: (x) -> throw x
12+
complete: -> expect(i).to.equal(tests.length)
13+
}
14+
15+
describe 'observable selectors: ', ->
16+
store = makeAStore()
17+
18+
RootComponent = null
19+
Subcomponent = null
20+
rootComponentInstance = null
21+
22+
describe 'simple: ', ->
23+
it 'should create subcomponent class', ->
24+
Subcomponent = createClass {
25+
mixins: [ ObservableSelectorMixin ]
26+
displayName: 'Subcomponent'
27+
verbs: ['SET']
28+
componentWillMount: ->
29+
console.log "Subcomponent.willMount: initial state", @state
30+
componentDidMount: ->
31+
console.log "Subcomponent.didMount"
32+
getReducer: (currentState) ->
33+
(state = {}, action) ->
34+
switch action.type
35+
when @SET then Object.assign({}, state, { payload: action.payload or {} })
36+
else state
37+
actionDispatchers: {
38+
setValue: (val) -> { type: @SET, payload: val }
39+
}
40+
selectors: {
41+
getValue: (state) ->
42+
state.payload
43+
}
44+
}
45+
46+
it 'should create new store', ->
47+
store = makeAStore({ foo: { payload: 'bar' } })
48+
49+
it 'should mount subcomponent', ->
50+
rootComponentInstance = createComponent( { foo: Subcomponent } )
51+
mountComponent(store, rootComponentInstance)
52+
53+
it 'should print the whole component tree for your viewing pleasure', ->
54+
console.log(inspect(rootComponentInstance))
55+
56+
it 'should observe after mutation by an action', ->
57+
subscription = rootComponentInstance.foo.getValue.subscribe(
58+
expectTestSequence([ ((x) -> x is 'bar'), ((x) -> x is 'hello world')] )
59+
)
60+
rootComponentInstance.foo.setValue('hello world')
61+
subscription.unsubscribe()
62+
63+
it 'should unsubscribe', ->
64+
subscription = rootComponentInstance.foo.getValue.subscribe(
65+
expectTestSequence([ ((x) -> x is 'hello world'), ((x) -> x is 'goodbye world')] )
66+
)
67+
rootComponentInstance.foo.setValue('goodbye world')
68+
subscription.unsubscribe()
69+
rootComponentInstance.foo.setValue('bar')

0 commit comments

Comments
 (0)