all files / src/ contextualData.cjsx

94.55% Statements 52/55
78.57% Branches 11/14
100% Functions 18/18
95.65% Lines 44/46
3 statements, 2 functions Ignored     
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228                                                                                                                                                                        21×   21×               21× 21×           33×     33× 33×       33× 33× 33×         33× 32×                 21×                                           21×   21× 21× 21× 21×             49×                   49×                     21× 21× 21× 21×       22×   22×     22×       21×       21×       20×                
 
React = require('react')
Backbone = require('backbone')
_ = require('underscore')
 
###
  This is an abstract base class for contextual data components like ReactDatum.Collection 
  and ReactDatum.Model that provide a single contextual data element.
  
  The ReactDatum.ContextualData base class also provides the listener to model or collection
  events and rendering of child components on changes.
  
  You shouldn't need to use this class directly.
###  
module.exports = class ContextualData extends React.Component
  #  don't forget your display name so you get semi-intelligent errors from react
  #@displayName: "react-datum.Model"
 
  ###
    This is the class of thing being placed in the context.
      ex. `Backbone.Model` or `Backbone.Collection`
  ###
  dataType:  null
  
  ###
   this is the key in @context children should use to access thing
    ex. "model"
  ###
  contextKey: null
 
 
  # you will also possibly want to extend these in your child class.  like this:
  # ```
  #   @proptypes: _.extend {}, ReactDatum.ContextualData.propTypes,
  #     collection: React.PropTypes.oneOfType([
  #       React.PropTypes.instanceOf(Backbone.Collection)
  #       React.PropTypes.func
  #     ]).isRequired
  # ```
  @propTypes:
    # set this to true to issue the initial fetch of the collection on mount
    fetch: React.PropTypes.bool
    # passed through to collection|model fetch
    fetchOptions: React.PropTypes.object
    # something to render if our input model|collection is not available
    placeholder: React.PropTypes.node  # anything react can render
    # additional css classes to add
    className: React.PropTypes.string
    # set debouncedUpdate = false to not debounce, i.e. 1:1 collection or
    # model triggered events to renders. 
    debouncedUpdate: React.PropTypes.bool
    # set debounceMs to a higher delay. 
    debounceMs: React.PropTypes.number
    # set to true to show console messages about useful things
    debug: React.PropTypes.bool
    # style override object for the rendered div.
    style: React.PropTypes.object
 
  # you will also need to similarly extend this, like this:
  #```
  #  @childContextTypes: _.extend {}, ReactDatum.ContextualData.childContextTypes,
  #    collection: React.PropTypes.instanceOf(Backbone.Collection)
  #```
  @childContextTypes: {}
  
  # extend these the same was as above or just replace them
  @defaultProps:
    # when true, we will automatically fetch the model or collection on mount
    fetch: false
    # additional fetch options (see Backbone Collection|Model fetch() options)
    fetchOptions: {}
    # We don't define placeholder, that's up to our subclass. with 
    # `placeholder: undefined` by default, there is no placeholder,
    # the renderContent method will always render children.  
    # To render no placeholder but not render children, set this to null
    placeholder: undefined
    # We do not define any default style data.
    style: {}
    # effectively batch and defer multiple syncronous events into one defaults to  
    # 0s debounce which will effectively ignore all but the last triggered event
    # in a long sequence
    debouncedUpdate: true
    debounceMs: 0
    
    
 
    
  constructor: (props) ->
    super props
    
    @state = 
      lastUpdated: null
      collectionOrModel: null
 
    # we don't want to delay, but we do want to head off a stampeed when
    # many events fire subsquently like after a large collection.add.
    #  
    # debounceMs is an undocumented prop because I'm not sure it's a good idea
    @debouncedUpdate = Eif @props.debouncedUpdate 
      _.debounce @update, @props.debounceMs
    else
      @update
 
 
  getChildContext: ->
    c = {}
    # it should be in state, if not we have a misunderstanding.  If you do it from the 
    # props and context too, might make it difficult to nullify
    c[@contextKey] = @state.collectionOrModel # || @props[@contextKey] || @context[@contextKey]
    return c
 
 
  render: ->
    className = "contextual-data #{@contextKey}"
    className += " #{@props.className}" if @props.className?
    return <span style={_.extend({}, @props.style)} className={className}>{@renderContent()}</span>
 
 
  # if the model we provide isn't set, render placeholder if user asked nicely
  renderContent: ->
    if @state.collectionOrModel? || @props.placeholder == undefined
      return @props.children
    
    return @props.placeholder
 
 
  ### !pragma coverage-skip-next ###
  componentWillUnmount: ->
    @unbindEvents()
    
    
  componentWillMount: ->
    @initializeCollectionOrModel()
    
  
  ### !pragma coverage-skip-next ###
  componentWillReceiveProps: (newProps)->
    @props = newProps
    @initializeCollectionOrModel()
    
    
  # api
  
 
  ###
    override this model to do a custom fetch method like fetchForUser or some such
  ###
  fetchCollectionOrModel: () ->
    @state.collectionOrModel.fetch(@props.fetchOptions) 
 
 
  ###
    extend this method to provide additional initialization on the 
    thing you provide.  You should probably call super
  ###
  initializeCollectionOrModel: () ->
    # we already have a model and the props model hasn't changed
    Ireturn unless @needsReinitializing()
 
    @unbindEvents()
    @setCollectionOrModel()
    @bindEvents()
    if @props.fetch && @state.collectionOrModel?
      @fetchCollectionOrModel()
  
      
  ###
    override this method to input from somewhere other than the context or props being passed in
  ###
  getInputCollectionOrModel: () ->
    @props[@contextKey] || @context[@contextKey]
  
  
  ###
    override or extend this method to provide something other than what we recieve
  ###  
  getCollectionOrModelToProvide: () ->
    # TODO : I think this should be `@state.collectionOrModel || @getInputCollectionOrModel()`
    #    that way an extension can just set the provided thing into state instead
    #    of being forced to override this method
    @getInputCollectionOrModel()
    
 
  ###
    extend this method to provide additional tests to determine if initialization is 
    needed.  You should probably extend this method like so:
    ```
      return super() || this._someOtherTest()
    ```
  ###
  needsReinitializing: () ->
    collectionOrModel = @getCollectionOrModelToProvide()
    truth = !@state.collectionOrModel? ||collectionOrModel != @_lastPropsModel
    @_lastPropsModel = collectionOrModel
    return truth
 
 
  setCollectionOrModel: () ->
    collectionOrModel = @getCollectionOrModelToProvide()
 
    @setState(collectionOrModel: collectionOrModel)
    # TODO : why do I need to do this.  @setState seems to not immediately take above
    # and later code on this path depends on this being set
    @state.collectionOrModel = collectionOrModel
 
 
  bindEvents: () ->
    @state.collectionOrModel?.on?('all', @onDataChanged, @)
 
 
  unbindEvents: () ->
    @state.collectionOrModel?.off?('all', @onDataChanged)
 
 
  onDataChanged: () =>
    @debouncedUpdate()
    
    
  update: () =>
    Iif @props.debug
      console.log "ContextualData: update on model", @state.collectionOrModel
    
    @setState(lastUpdated: Date.now(), collectionOrModel: @getCollectionOrModelToProvide())
    if @props.forceUpdate
      @forceUpdate()