UNPKG

atom-react

Version:

An opiniated way to use ReactJS in a functional way in plain old Javascript, inspired by popular Clojurescript wrappers like Om

303 lines (235 loc) 8.57 kB
'use strict'; var React = require("react"); var ReactLink = require("react/lib/ReactLink"); var Preconditions = require("./utils/preconditions"); var DeepFreeze = require("./utils/deepFreeze"); var Immutables = require("./utils/immutables"); var ArgumentsOrArray = require("./utils/argumentsOrArray"); var Atom = require("./atom/atom"); var AtomReactContext = require("./atomReactContext"); var AtomReactStore = require("./atomReactStore"); var AtomCursor = require("./atom/atomCursor"); var AtomAsyncValue = require("./atom/atomAsyncUtils").AtomAsyncValue; var AtomReactEvent = require("./atomReactEvent"); var _ = require("lodash"); function isSameValueOrCursor(value,nextValue) { if ( value instanceof AtomCursor && nextValue instanceof AtomCursor ) { return value.creationTimeValue === nextValue.creationTimeValue; } else { return value === nextValue; } } // This is the "shallowEqual" from React, a little bit modified to handle cursors function shallowEqual(objA, objB) { if (objA === objB) { return true; } var key; // Test for A's keys different from B. for (key in objA) { if ( objA.hasOwnProperty(key) && (!objB.hasOwnProperty(key) || !isSameValueOrCursor(objA[key],objB[key])) ) { return false; } } // Test for B's keys missing from A. for (key in objB) { if ( objB.hasOwnProperty(key) && !objA.hasOwnProperty(key) ) { return false; } } return true; } var WithPureRenderMixin = { shouldComponentUpdate: function(nextProps, nextState) { return !shallowEqual(this.props,nextProps) || !shallowEqual(this.state,nextState); } }; exports.WithPureRenderMixin = WithPureRenderMixin; var doLogNonAtomReactWarning = function() { console.warn("Hey! It seems your current application does not use AtomReact nor provide actions in context." + "It is not allowed to use AtomReact components inside a non-AtomReact app!" + "AtomReact being deprecated you should rather use Redux instead"); doLogNonAtomReactWarning = function() { }; // NOOP: log only once! }; var WithActionsMixin = { contextTypes: { atomReactContext: React.PropTypes.object, actions: React.PropTypes.object }, componentWillMount: function() { var contextActions = this.context.atomReactContext ? this.context.atomReactContext.actions : undefined; var actions = this.context.actions || contextActions; if ( !actions ) { doLogNonAtomReactWarning(); } else { this.actions = actions; } } }; exports.WithActionsMixin = WithActionsMixin; exports.connectActions = function connectActionsHOC(WrappedComponent) { var displayName = (function() { var originalDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; return "connectActions("+originalDisplayName+")"; })(); return React.createClass({ displayName: displayName, contextTypes: { atomReactContext: React.PropTypes.object, actions: React.PropTypes.object }, render: function() { var contextActions = this.context.atomReactContext ? this.context.atomReactContext.actions : undefined; var actions = this.context.actions || contextActions; if ( !actions ) { doLogNonAtomReactWarning(); } var injectedProps = { actions: actions }; var props = _.assign({},this.props,injectedProps); return React.createElement(WrappedComponent,props, undefined); } }); }; var WithCursorLinkingMixin = { linkCursor: function(cursor) { return new ReactLink( cursor.getOrElse(undefined), function setCursorNewValue(value) { cursor.set(value); } ); } }; exports.WithCursorLinkingMixin = WithCursorLinkingMixin; var WithTransactMixin = { contextTypes: { atom: React.PropTypes.instanceOf(Atom) }, transact: function(tasks) { this.context.atom.transact(tasks); } }; exports.WithTransactMixin = WithTransactMixin; var WithEventPublisherMixin = { contextTypes: { publishEvents: React.PropTypes.func }, publish: function() { var array = ArgumentsOrArray(arguments); this.context.publishEvents(array); } }; exports.WithEventPublisherMixin = WithEventPublisherMixin; var WithEventListenerMixin = { contextTypes: { addEventListener: React.PropTypes.func, removeEventListener: React.PropTypes.func }, addEventListener: function(listener) { this.context.addEventListener(listener); }, removeEventListener: function(listener) { this.context.removeEventListener(listener); }, componentDidMount: function() { if ( this.listenToEvents ) { this.context.addEventListener(this.listenToEvents); } }, componentWillUnmount: function() { if ( this.listenToEvents ) { this.context.removeEventListener(this.listenToEvents); } } }; exports.WithEventListenerMixin = WithEventListenerMixin; function getAllCursors(props) { return Object.keys(props) .map(function(key) { return props[key]; }) .filter(function(value) { return value instanceof AtomCursor; }); } // As during the whole render, the cursor values are not supposed // to change we memoize them to the value they had at creation time function memoizeCursor(cursor) { cursor.memoizeToCreationTimeValue(); } // But we unmemoize the cursors outside the render to provide read-your-writes semantics in callbacks like // componentDidMount, componentDidUpdate, timers, intervals etc... function unmemoizeCursor(cursor) { cursor.unmemoize(); } // This memoizes the cursors just before the render method is called. // Cursors are then unmemoized to provide read-your-writes semantics var WithCursorsMemoizationMixin = { componentWillMount: function() { getAllCursors(this.props).forEach(memoizeCursor); }, componentDidMount: function() { getAllCursors(this.props).forEach(unmemoizeCursor); }, componentWillUpdate: function(nextProps) { getAllCursors(nextProps).forEach(memoizeCursor); }, componentDidUpdate: function() { getAllCursors(this.props).forEach(unmemoizeCursor); } }; exports.WithCursorsMemoizationMixin = WithCursorsMemoizationMixin; function addMixins(config) { config.mixins = config.mixins || []; config.mixins.push(WithCursorsMemoizationMixin); config.mixins.push(WithPureRenderMixin); config.mixins.push(WithCursorLinkingMixin); config.mixins.push(WithTransactMixin); config.mixins.push(WithEventPublisherMixin); config.mixins.push(WithEventListenerMixin); config.mixins.push(WithActionsMixin); } function createPureClass() { var name; var component; if ( arguments.length === 2 ) { name = arguments[0]; component = arguments[1]; } // Just to be retrocompatible with existing React components createClass... else { name = "AtomReactPureClass"; component = arguments[0]; } // Unfortunately, the displayName can't be infered from the variable name during JSX compilation :( // See http://facebook.github.io/react/docs/component-specs.html#displayname component.displayName = name; // Because React's displayName is not easy to obtain from a mixin (???) component.getDisplayName = function() { return name }; addMixins(component); return React.createClass(component); } exports.createPureClass = createPureClass; exports.createClass = createPureClass; var PropTypes = { isCursor: React.PropTypes.instanceOf(AtomCursor).isRequired, isOptionalCursor: React.PropTypes.instanceOf(AtomCursor), isAsyncValue: React.PropTypes.instanceOf(AtomAsyncValue).isRequired }; exports.PropTypes = PropTypes; function newContext() { return new AtomReactContext(); } exports.newContext = newContext; function newStore(name,description) { return new AtomReactStore.AtomReactStore(name,description); } exports.newStore = newStore; exports.Preconditions = Preconditions; exports.DeepFreeze = DeepFreeze; exports.Event = AtomReactEvent; exports.EmptyArray = Immutables.EmptyArray; exports.EmptyObject = Immutables.EmptyObject;