|
| 1 | +import { Component, createElement } from 'react'; |
| 2 | +import hoistStatics from 'hoist-non-react-statics'; |
| 3 | +import isPlainObject from '../utils/isPlainObject'; |
| 4 | +import shallowEqual from '../utils/shallowEqual'; |
| 5 | +import pubSubShape from '../shapes/pubSubShape'; |
| 6 | + |
| 7 | +const defaultMapPublishToProps = publish => ({ publish }); |
| 8 | +const defaultMapSubscriptionsToProps = () => ({}); |
| 9 | +const defaultInitMapSubscriptionsToProps = () => defaultMapSubscriptionsToProps; |
| 10 | + |
| 11 | +function getDisplayName(WrappedComponent) { |
| 12 | + return WrappedComponent.displayName || WrappedComponent.name || 'Component'; |
| 13 | +} |
| 14 | + |
| 15 | +function cleanEmptyKeys(obj = {}) { |
| 16 | + Object.keys(obj) |
| 17 | + .filter(key => obj[key] === undefined || obj[key] === null) |
| 18 | + .forEach(key => delete obj[key]); |
| 19 | + return obj; |
| 20 | +} |
| 21 | + |
| 22 | +function wrapSubscritionsMap(mapSubscriptionsToProps) { |
| 23 | + const validMappedSubscriptions = Object.keys(mapSubscriptionsToProps) |
| 24 | + .every( |
| 25 | + key => (typeof mapSubscriptionsToProps[key] === 'function' || |
| 26 | + typeof mapSubscriptionsToProps[key] === 'string') |
| 27 | + ); |
| 28 | + |
| 29 | + if (!validMappedSubscriptions) { |
| 30 | + throw new Error( |
| 31 | + `Every mapped Subscription of "createPubSubConnector" must be a function` |
| 32 | + + `returning the value to be passed as prop to the decorated component.` |
| 33 | + ); |
| 34 | + } |
| 35 | + |
| 36 | + return (pubSub, notifyChange, getProps) => { |
| 37 | + const { add } = pubSub; |
| 38 | + let map = {}; |
| 39 | + |
| 40 | + const updateStoredMapFromObject = (updatedValue) => { |
| 41 | + const updatedMap = {}; |
| 42 | + const keysToUpdate = Object.keys(updatedValue) |
| 43 | + .filter(key => { |
| 44 | + if (map.hasOwnProperty(key)) { |
| 45 | + if (updatedValue[key] && shallowEqual(map[key], updatedValue[key])) { |
| 46 | + return false; |
| 47 | + } |
| 48 | + } |
| 49 | + updatedMap[key] = updatedValue[key]; |
| 50 | + return true; |
| 51 | + }); |
| 52 | + |
| 53 | + if (keysToUpdate.length) { |
| 54 | + map = cleanEmptyKeys(Object.assign({}, map, updatedMap)); |
| 55 | + return true; |
| 56 | + } |
| 57 | + return false; |
| 58 | + }; |
| 59 | + |
| 60 | + const updateStoredMapFromKey = (key, updatedValue) => { |
| 61 | + if (map.hasOwnProperty(key)) { |
| 62 | + if (updatedValue && shallowEqual(map[key], updatedValue)) { |
| 63 | + return false; |
| 64 | + } |
| 65 | + } |
| 66 | + map = cleanEmptyKeys(Object.assign({}, map, { [key]: updatedValue })); |
| 67 | + return true; |
| 68 | + }; |
| 69 | + |
| 70 | + Object |
| 71 | + .keys(mapSubscriptionsToProps) |
| 72 | + .forEach(key => { |
| 73 | + const transformerOrAlias = mapSubscriptionsToProps[key]; |
| 74 | + if (typeof transformerOrAlias === 'function') { |
| 75 | + add(key, (...args) => { |
| 76 | + const updatedValue = transformerOrAlias(...args, getProps()); |
| 77 | + if (updateStoredMapFromObject(updatedValue)) { |
| 78 | + notifyChange(); |
| 79 | + } |
| 80 | + }); |
| 81 | + } else { |
| 82 | + add(key, payload => { |
| 83 | + if (updateStoredMapFromKey(transformerOrAlias, payload)) { |
| 84 | + notifyChange(); |
| 85 | + } |
| 86 | + }); |
| 87 | + } |
| 88 | + }); |
| 89 | + |
| 90 | + return () => map; |
| 91 | + }; |
| 92 | +} |
| 93 | + |
| 94 | +function wrapPublishMethods(mapPublishToProps) { |
| 95 | + return publish => Object.keys(mapPublishToProps) |
| 96 | + .filter(key => typeof mapPublishToProps[key] === 'function') |
| 97 | + .reduce((acc, key) => { |
| 98 | + acc[key] = (...args) => mapPublishToProps[key](publish, ...args); |
| 99 | + return acc; |
| 100 | + }, {}); |
| 101 | +} |
| 102 | + |
| 103 | +export default function createPubSubConnector(mapSubscriptionsToProps, mapPublishToProps, options = {}) { |
| 104 | + if ( |
| 105 | + mapSubscriptionsToProps && |
| 106 | + (typeof mapSubscriptionsToProps !== 'function' && !isPlainObject(mapSubscriptionsToProps)) |
| 107 | + ) { |
| 108 | + throw new Error( |
| 109 | + `"createPubSubConnector" expected "mapSubscriptionsToProps" to be a function` |
| 110 | + + ` or a plain object, instead received: ${mapSubscriptionsToProps}.` |
| 111 | + ); |
| 112 | + } |
| 113 | + const shouldMapSubscriptions = isPlainObject(mapSubscriptionsToProps) ? Object.keys(mapSubscriptionsToProps).length : Boolean(mapSubscriptionsToProps); |
| 114 | + |
| 115 | + let finalMapSubscriptionsToProps = defaultMapSubscriptionsToProps; |
| 116 | + let doSubscribedPropsDependOnOwnProps = false; |
| 117 | + |
| 118 | + |
| 119 | + const finalMapPublishToProps = isPlainObject(mapPublishToProps) ? wrapPublishMethods(mapPublishToProps) : mapPublishToProps || defaultMapPublishToProps; |
| 120 | + const doPublishPropsDependOnOwnProps = finalMapPublishToProps.length > 1; |
| 121 | + |
| 122 | + const { withRef = false } = options || {}; |
| 123 | + |
| 124 | + function initComputeSubscribedProps(pubSub, notifier, getProps) { |
| 125 | + let subscriber; |
| 126 | + if (isPlainObject(mapSubscriptionsToProps)) { |
| 127 | + subscriber = wrapSubscritionsMap(mapSubscriptionsToProps); |
| 128 | + } else { |
| 129 | + subscriber = mapSubscriptionsToProps || defaultInitMapSubscriptionsToProps; |
| 130 | + } |
| 131 | + finalMapSubscriptionsToProps = subscriber(pubSub, notifier, getProps); |
| 132 | + doSubscribedPropsDependOnOwnProps = finalMapSubscriptionsToProps.length > 0; |
| 133 | + } |
| 134 | + |
| 135 | + function computeSubscribedProps(props) { |
| 136 | + const subscribedProps = doSubscribedPropsDependOnOwnProps ? finalMapSubscriptionsToProps(props) : finalMapSubscriptionsToProps(); |
| 137 | + |
| 138 | + if (!isPlainObject(subscribedProps)) { |
| 139 | + throw new Error( |
| 140 | + `'mapSubscriptionsToProps' must return an object.` |
| 141 | + + `Instead received ${subscribedProps}` |
| 142 | + ); |
| 143 | + } |
| 144 | + return subscribedProps; |
| 145 | + } |
| 146 | + |
| 147 | + function computePublishProps(pubSub, props) { |
| 148 | + const { publish } = pubSub; |
| 149 | + const publishProps = doPublishPropsDependOnOwnProps ? finalMapPublishToProps(publish, props) : finalMapPublishToProps(publish); |
| 150 | + |
| 151 | + if (!isPlainObject(publishProps)) { |
| 152 | + throw new Error( |
| 153 | + `'mapPublishToProps' must return an object.` |
| 154 | + + `Instead received ${publishProps}` |
| 155 | + ); |
| 156 | + } |
| 157 | + return publishProps; |
| 158 | + } |
| 159 | + |
| 160 | + return function wrapComponent(Composed) { |
| 161 | + class PubSubConnector extends Component { |
| 162 | + |
| 163 | + constructor(props, context) { |
| 164 | + super(props, context); |
| 165 | + this.pubSubCore = props.pubSubCore || context.pubSubCore; |
| 166 | + |
| 167 | + if (!this.pubSubCore) { |
| 168 | + throw new Error( |
| 169 | + `Could not find "pubSubCore" in either the context or ` |
| 170 | + + `props of "${this.constructor.displayName}". ` |
| 171 | + + `Either wrap the root component in a <PubSubProvider>, ` |
| 172 | + + `or explicitly pass "pubSubCore" as a prop to "${this.constructor.displayName}".` |
| 173 | + ); |
| 174 | + } |
| 175 | + this.state = {}; |
| 176 | + this.pubSub = this.pubSubCore.register(this); |
| 177 | + } |
| 178 | + |
| 179 | + componentWillMount() { |
| 180 | + this.trySubscribe(); |
| 181 | + } |
| 182 | + |
| 183 | + componentWillReceiveProps(nextProps) { |
| 184 | + if (!shallowEqual(nextProps, this.props)) { |
| 185 | + this.haveOwnPropsChanged = true; |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + shouldComponentUpdate() { |
| 190 | + return this.haveOwnPropsChanged || this.hasSubscribedPropsChanged; |
| 191 | + } |
| 192 | + |
| 193 | + componentWillUnmount() { |
| 194 | + this.pubSub.unsubscribe(); |
| 195 | + } |
| 196 | + |
| 197 | + getWrappedInstance() { |
| 198 | + if (!withRef) { |
| 199 | + throw new Error( |
| 200 | + `To access the wrapped instance, you need to specify explicitly` |
| 201 | + + ` { withRef: true } in the options passed to the createPubSubConnector() call.` |
| 202 | + ); |
| 203 | + } |
| 204 | + return this.refs.wrappedInstance; |
| 205 | + } |
| 206 | + |
| 207 | + hasSubscriptions() { |
| 208 | + if (this.pubSub) { |
| 209 | + return this.pubSub.subscriptions.length ? true : false; |
| 210 | + } |
| 211 | + return false; |
| 212 | + } |
| 213 | + |
| 214 | + notifyChange() { |
| 215 | + const prevSubscribedProps = this.state.subscribedProps; |
| 216 | + const subscribedProps = finalMapSubscriptionsToProps(this.props); |
| 217 | + |
| 218 | + if (prevSubscribedProps !== subscribedProps) { |
| 219 | + this.hasSubscribedPropsChanged = true; |
| 220 | + this.setState({ subscribedProps }); |
| 221 | + } |
| 222 | + } |
| 223 | + |
| 224 | + trySubscribe() { |
| 225 | + if (shouldMapSubscriptions) { |
| 226 | + initComputeSubscribedProps( |
| 227 | + this.pubSub, () => this.notifyChange(), () => this.props |
| 228 | + ); |
| 229 | + this.notifyChange(); |
| 230 | + } |
| 231 | + } |
| 232 | + |
| 233 | + updateSubscribedPropsIfNeeded() { |
| 234 | + const nextSubscribedProps = computeSubscribedProps(this.props); |
| 235 | + if (this.subscribedProps && shallowEqual(nextSubscribedProps, this.subscribedProps)) { |
| 236 | + return false; |
| 237 | + } |
| 238 | + |
| 239 | + this.subscribedProps = nextSubscribedProps; |
| 240 | + return true; |
| 241 | + } |
| 242 | + |
| 243 | + updatePublishPropsIfNeeded() { |
| 244 | + const nextPublishProps = computePublishProps(this.pubSub, this.props); |
| 245 | + if (this.publishProps && shallowEqual(nextPublishProps, this.publishProps)) { |
| 246 | + return false; |
| 247 | + } |
| 248 | + |
| 249 | + this.publishProps = nextPublishProps; |
| 250 | + return true; |
| 251 | + } |
| 252 | + |
| 253 | + render() { |
| 254 | + const { |
| 255 | + haveOwnPropsChanged, |
| 256 | + hasSubscribedPropsChanged, |
| 257 | + renderedElement, |
| 258 | + pubSub, |
| 259 | + } = this; |
| 260 | + |
| 261 | + this.haveOwnPropsChanged = false; |
| 262 | + this.hasSubscribedPropsChanged = false; |
| 263 | + |
| 264 | + let shouldUpdateSubscribedProps = true; |
| 265 | + let shouldUpdatePublishProps = true; |
| 266 | + if (renderedElement) { |
| 267 | + shouldUpdateSubscribedProps = hasSubscribedPropsChanged || (haveOwnPropsChanged && doSubscribedPropsDependOnOwnProps); |
| 268 | + shouldUpdatePublishProps = haveOwnPropsChanged && doPublishPropsDependOnOwnProps; |
| 269 | + } |
| 270 | + |
| 271 | + let haveSubscribedPropsChanged = false; |
| 272 | + let havePublishPropsChanged = false; |
| 273 | + if (shouldUpdateSubscribedProps) { |
| 274 | + haveSubscribedPropsChanged = this.updateSubscribedPropsIfNeeded(); |
| 275 | + } |
| 276 | + if (shouldUpdatePublishProps) { |
| 277 | + havePublishPropsChanged = this.updatePublishPropsIfNeeded(); |
| 278 | + } |
| 279 | + |
| 280 | + if ( |
| 281 | + !haveSubscribedPropsChanged && |
| 282 | + !havePublishPropsChanged && |
| 283 | + !haveOwnPropsChanged && |
| 284 | + renderedElement |
| 285 | + ) { |
| 286 | + return renderedElement; |
| 287 | + } |
| 288 | + |
| 289 | + const baseProps = { pubSub }; |
| 290 | + if (withRef) { |
| 291 | + Object.assign(baseProps, { ref: 'wrappedInstance' }); |
| 292 | + } |
| 293 | + |
| 294 | + const mergedProps = Object.assign( |
| 295 | + {}, |
| 296 | + this.props, |
| 297 | + this.subscribedProps, |
| 298 | + this.publishProps, |
| 299 | + baseProps |
| 300 | + ); |
| 301 | + |
| 302 | + this.renderedElement = createElement(Composed, mergedProps); |
| 303 | + |
| 304 | + return this.renderedElement; |
| 305 | + } |
| 306 | + } |
| 307 | + |
| 308 | + PubSubConnector.contextTypes = { |
| 309 | + pubSubCore: pubSubShape, |
| 310 | + }; |
| 311 | + |
| 312 | + PubSubConnector.propTypes = { |
| 313 | + pubSubCore: pubSubShape, |
| 314 | + }; |
| 315 | + |
| 316 | + PubSubConnector.displayName = `PubSubConnector(${getDisplayName(Composed)})`; |
| 317 | + PubSubConnector.WrappedComponent = Composed; |
| 318 | + |
| 319 | + return hoistStatics(PubSubConnector, Composed); |
| 320 | + }; |
| 321 | +} |
0 commit comments