When the new guy starts writing in CoffeeScript
This is Morgan
yourgrocer.com.au
Rails app built on
Traditional web app with jQuery
Existing JSON API
Back to school
Facebook's React Tutorial
Learn React, Flux, and Flow: Part I - @colinmegill
React for stupid people - @andrewray
Flux for stupid people - @andrewray
Gmail
Flux in 18 lines
window.App =
Components: {}
Actions: {}
Stores: {}
App.Dispatcher = MicroEvent.mixin
register: (events)->
for eventName, callback of events
@bind eventName, callback
Actions =
dispatch: (name, payload)->
App.Dispatcher.trigger name, payload
App.createActions = (obj)->
Object.assign(Object.create(Actions), obj)
Store =
emitChange: -> @trigger 'change'
App.createStore = (obj)->
store = Object.assign(Object.create(Store), obj)
MicroEvent.mixin store
Components initiate Actions
{ CartActions } = App.Actions
App.Components.CartItem = React.createClass
increment: ->
CartActions.updateQuantity(@props.id, @props.quantity + 1)
render: ->
<a onClick={@increment}>
<i className="icon icon-chevron-up" />
</a>
Actions dispatch an event with a unique name and payload
App.Actions.CartActions = App.createActions
updateQuantity: (id, quantity)->
@dispatch 'cart-update-line-item-quantity', id, quantity
Actions are a good place to make requests
App.Actions.CartActions = App.createActions
updateQuantity: (id, quantity)->
@dispatch 'cart-update-line-item-quantity', id, quantity
updateQuantity(id, quantity)
# each lineItem can have a timeout
timeouts = {}
updateQuantity = (id, quantity)->
clearTimeout(timeouts[id]) if timeouts[id]?
timeouts[id] = setTimeout ->
SpreeAPI.updateQuantity id, quantity
, 500
Stores
_order =
lineItems: []
App.Stores.CartStore = CartStore = App.createStore
getState: ->
order: _order
updateQuantity = (id, quantity)->
item = _order.lineItems.find (i)-> i.id == id
item.quantity = quantity if item?
CartStore.emitChange()
App.Dispatcher.register
'cart-update-line-item-quantity': (id, quantity)->
updateQuantity(id, quantity)
Components
{ CartStore } = App.Stores
App.Components.CartContainer = React.createClass
getInitialState: ->
CartStore.getState()
componentDidMount: ->
CartStore.bind 'change', @onChange
componentWillUnmount: ->
CartStore.unbind 'change', @onChange
onChange: ->
@setState CartStore.getState()
Container Components
{ CartStore } = App.Stores
{ Cart } = App.Components
App.Components.CartContainer = React.createClass
getInitialState: ->
CartStore.getState()
componentDidMount: ->
CartStore.bind 'change', @onChange
componentWillUnmount: ->
CartStore.unbind 'change', @onChange
onChange: ->
@setState CartStore.getState()
render: ->
# separate data fetching / rendering
<Cart {...@state} />
So that's data
Computation
CartItem
{ currency } = App.Filters
App.Components.CartItem = React.createClass
render: ->
<div className={classNames('cart-item row', { 'cart-item-removed': @props.quantity == 0 })}>
...
<div className="col">
<QuantitySpinner quantity={@props.quantity} min=1 onIncrement={@increment} onDecrement={@decrement} />
</div>
<div className="col">
<strong className="pull-right">{currency(@props.price * @props.quantity)}</strong>
</div>
<div className="col">
<a className="btn btn-sm btn-danger pull-right" onClick={@remove}>
<i className="icon icon-trash" />
</a>
</div>
</div>
CartSummary
{ currency } = App.Filters
App.Components.CartSummary = React.createClass
render: ->
subTotal = _.sum @props.order.lineItems, (lineItem)->
lineItem.price * lineItem.quantity
gstTotal = @props.order.taxTotal
deliveryTotal = @props.order.estimatedShipTotal
adjustmentsTotal = _.sum @props.order.adjustments, (a)-> a.amount
estimatedTotal = subTotal + deliveryTotal + adjustmentsTotal
pendingClass = classNames('pending-update': @props.states.totalsPendingUpdate)
<div className="cart-summary">
<div className="total-item row">
<div className="total-label">Groceries</div>
<div className="total-value">{ currency(subTotal) }</div>
</div>
<div className={ "total-item row #{ pendingClass }"}>
<div className="total-label">Delivery fee</div>
<div className="total-value">{ currency(deliveryTotal) }</div>
</div>
{ for adjustment in @props.order.adjustments
<div className={ "total-item row #{ pendingClass }"}>
<div className="total-label">{ adjustment.label }</div>
<div className="total-value">{ currency(adjustment.amount) }</div>
</div>
}
<div className={ "total-item row #{ pendingClass }"}>
<div className="total-label">Estimated total</div>
<div className="total-value">{ currency(estimatedTotal) }</div>
</div>
</div>
States on Stores
_order =
lineItems: []
_states =
pendingUpdate: false
App.Stores.CartStore = CartStore = App.createStore
getState: ->
order: _order
states: _states
updateQuantity = (id, quantity)->
item = _order.lineItems.find (i)-> i.id == id
item.quantity = quantity if item?
_states.pendingUpdate = true
CartStore.emitChange()
updateTotals = (data)->
_order = data.order
_states.pendingUpdate = false
CartStore.emitChange()
App.Dispatcher.register
'cart-update-line-item-quantity': (id, quantity)-> updateQuantity(id, quantity)
'order-totals-updated': (data)-> updateTotals(data)
CartSummaryContainer
Bootstrapping stores
#cart-component
= react_component 'App.Components.CartContainer'
:coffee
App.Stores.CartStore.bootstrap
#{ render('orders/_order.jbuilder') }
App.Stores.CartStore = CartStore = App.createStore
bootstrap: (data)->
_order = data.order
@emitChange()
Testing Filters
{ currency } = App.Filters
describe 'App.Filters', ->
describe 'currency()', ->
it 'should return empty string if NaN', ->
expect(currency('boo')).to.equal('')
it 'should return formatted dollars and cents', ->
expect(currency(12.152)).to.equal('$12.15')
expect(currency('12.152')).to.equal('$12.15')
expect(currency(-12)).to.equal('-$12.00')
expect(currency('-12')).to.equal('-$12.00')
Testing Actions / Stores
{ CartActions } = App.Actions
{ CartStore } = App.Stores
server = null
clock = null
order = null
describe 'CartActions', ->
before ->
server = sinon.fakeServer.create()
clock = sinon.useFakeTimers()
after ->
server.restore()
clock.restore()
beforeEach ->
order = chai.create('order')
CartStore.bootstrap(order: order)
describe 'incrementQuantity', ->
it 'should increment total quantity in CartStore', ->
totalQty = _.sum order.line_items, 'quantity'
lineItem = order.line_items[0]
CartActions.incrementQuantity(lineItem.id, lineItem.quantity + 1)
newOrder = CartStore.getState().order
newTotalQty = _.sum newOrder.line_items, 'quantity'
expect(newTotalQty).to.equal(totalQty + 1)
describe 'decrementQuantity', ->
it 'should decrement quantity in CartStore', ->
totalQty = _.sum order.line_items, 'quantity'
lineItem = order.line_items[0]
CartActions.decrementQuantity(lineItem.id, lineItem.quantity - 1)
newOrder = CartStore.getState().order
newTotalQty = _.sum newOrder.line_items, 'quantity'
expect(newTotalQty).to.equal(totalQty - 1)
describe 'removeLineItem', ->
it 'should set quantity to 0 in CartStore', ->
lineItem = order.line_items[0]
CartActions.removeLineItem(lineItem.id)
newOrder = CartStore.getState().order
lineItem = newOrder.line_items.find (l)-> l.id == lineItem.id
expect(lineItem.quantity).to.equal(0)
describe 'applyCoupon', ->
it 'should add an adjustment CartStore', ->
CartActions.applyCoupon('IKNOWPETERPAN')
# PUT /shop/api/orders/:id/apply_coupon_code
server.requests[0].respond 200, { "Content-Type": "application/json" }, JSON.stringify
success: "The coupon code was successfully applied to your order."
error: null
server.requests.shift()
# GET /shop/api/orders/:id
order.adjustments.push
eligible: true
amount: '-4.00'
label: 'Free delivery'
server.requests[0].respond 200, { "Content-Type": "application/json" }, JSON.stringify(order)
server.requests.shift()
newOrder = CartStore.getState().order
expect(newOrder.adjustments.length).to.equal(order.adjustments.length)
it 'should expose an error if the coupon is no good', ->
CartActions.applyCoupon('IKNOWDARTH')
server.requests[0].respond 200, { "Content-Type": "application/json" }, JSON.stringify
success: null
error: "The coupon code was invalid."
server.requests.shift()
cartStates = CartStore.getState().states
expect(cartStates.couponError).to.equal("The coupon code was invalid.")
Thoughts so far
Flux and React work well
React's tiny API is great
render() is messier than templates
There's an art to Components and Stores
Worth following
Thanks
@markbrown4