All files / src ThroughProvider.js

95.83% Statements 46/48
81.82% Branches 27/33
100% Functions 14/14
95.65% Lines 44/46
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                              24x 24x       29x                   292x 24x           292x       122x   122x 18x         24x   24x 24x   24x 22x 22x           60x   60x 60x 132x         60x       146x 146x                         82x 82x   82x   82x 22x   22x   10x   60x   60x   54x         64x 64x 64x 64x       64x 64x   64x 58x 58x 58x 58x       29x      
import React, { Children } from 'react'
import PropTypes from 'prop-types'
 
 
export default class ThroughProvider extends React.Component {
  static childContextTypes = {
    through: PropTypes.object.isRequired,
  }
 
  static propTypes = {
    shouldBreadcrumbsUpdate: PropTypes.func,
    children: PropTypes.element,
  }
 
  constructor(props) {
    super(props)
    this.areas = {}
  }
 
  getChildContext() {
    return {
      through: {
        install: this.install,
        remove: this.remove,
        subscribe: this.subscribe,
      }
    }
  }
 
  area = (area) => {
    if( !this.areas.hasOwnProperty(area) ) {
      this.areas[area] = {
        name: area,
        listeners: [],
        data: {},
      }
    }
    return this.areas[area]
  }
 
  notify = (area, syncUpdate) => {
    area = this.area(area)
 
    area.listeners.forEach(
      listener => listener(area.data, syncUpdate)
    )
  }
 
  subscribe = ( area, listener ) => {
    area = this.area(area)
 
    area.listeners.push(listener)
    listener(area.data)
 
    return () => {
      area.listeners = area.listeners.filter(
        item => item !== listener
      )
    }
  }
 
  _diffAndComplicatedCounts(prevProps, props) {
    const keys = Object.keys(prevProps).concat(Object.keys(props))
    // reflect types in the documentation if changed
    const quicks = [ 'string', 'number', 'undefined', 'boolean', 'symbol' ]
    const [diff, comp] = keys.reduce( (stat,k) => {
      return [
        stat[0] + (prevProps[k] !== props[k] ? 1 : 0),
        stat[1] + (!quicks.includes(typeof(props[k])) ? 1 : 0)
      ]
    }, [0, 0])
    return [diff, comp]
  }
 
  checkArgs(area, key, props) {
    Eif (process.env.NODE_ENV !== 'production') {
      Iif(
        !( typeof area === 'string' || area instanceof String ) ||
        !( typeof key === 'string' || key instanceof String ) ||
        !( props instanceof Object && !(props instanceof Array) )
      ) {
        throw new Error(
          "type error: through.[install|remove](area:string, key:string, props:Object)"
        )
      }
    }
  }
 
  install = (area, key, props, syncUpdate = undefined) => {
    this.checkArgs(area, key, props)
    area = this.area(area)
 
    const prevProps = area.data[key] || {}
 
    if( this.props.shouldBreadcrumbsUpdate ) {
      const diff = this.props.shouldBreadcrumbsUpdate(prevProps, props)
 
      if( !diff ) return
 
      syncUpdate = true
    } else {
      const [diff, comp] = this._diffAndComplicatedCounts(prevProps, props)
 
      if( !diff ) return
 
      Iif( undefined === syncUpdate ) {
        syncUpdate = !comp
      }
    }
 
    const data = Object.assign({}, area.data)
    data[key] = {...props}
    area.data = data
    this.notify(area.name, syncUpdate)
  }
 
  remove = (area, key, syncUpdate = undefined) => {
    this.checkArgs(area, key, {})
    area = this.area(area)
 
    if( area.data.hasOwnProperty(key) ) {
      const data = Object.assign({}, area.data)
      delete data[key]
      area.data = data
      this.notify(area.name, true)
    }
  }
  render() {
    return React.Children.only(this.props.children)
  }
}