import * as React from 'react'; import { createElement as h } from 'react' import * as PropTypes from 'prop-types'; import { Plan, Xcomponent, XcomponentClass, ContextEngine, XREACT_ENGINE, Update, Actions, Xprops } from './interfaces' import { streamOps, Stream, Subject } from './xs' export const CONTEXT_TYPE = { [XREACT_ENGINE]: PropTypes.shape({ intent$: PropTypes.object, history$: PropTypes.object }) }; function isSFC(Component: React.ComponentClass | React.SFC): Component is React.SFC { return (typeof Component == 'function') } export function extendXComponentClass(WrappedComponent: XcomponentClass, main: Plan): XcomponentClass { return class XNode extends WrappedComponent { static contextTypes = CONTEXT_TYPE static displayName = `X(${getDisplayName(WrappedComponent)})` constructor(props: Xprops, context: ContextEngine) { super(props, context); let engine = context[XREACT_ENGINE] let { actions, update$ } = main(engine.intent$, props) this.machine.update$ = streamOps.merge, Update>(this.machine.update$, update$) if (actions) this.machine.actions = Object.assign({}, bindActions(actions, engine.intent$, this), this.machine.actions) } } } export function genXComponentClass(WrappedComponent: React.ComponentType, main: Plan, opts?: any): XcomponentClass { return class XLeaf extends Xcomponent { static contextTypes = CONTEXT_TYPE static displayName = `X(${getDisplayName(WrappedComponent)})` defaultKeys: (keyof S)[] constructor(props: Xprops, context: ContextEngine) { super(props, context); let engine = context[XREACT_ENGINE] let { actions, update$ } = main(engine.intent$, props) this.machine = { update$: update$ } this.machine.actions = bindActions(actions || {}, engine.intent$, this) this.defaultKeys = WrappedComponent.defaultProps ? (<(keyof S)[]>Object.keys(WrappedComponent.defaultProps)) : []; this.state = (Object.assign( {}, WrappedComponent.defaultProps, >pick(this.defaultKeys, props) )); } componentWillReceiveProps(nextProps: I) { this.setState((state, props) => Object.assign({}, nextProps, pick(this.defaultKeys, state))); } componentDidMount() { this.subscription = streamOps.subscribe( this.machine.update$, (action: Update) => { if (action instanceof Function) { if (process.env.NODE_ENV == 'debug') console.log('UPDATE:', action) this.setState((prevState, props) => { let newState: S = action.call(this, prevState, props); this.context[XREACT_ENGINE].history$.next(newState) if (process.env.NODE_ENV == 'debug') console.log('STATE:', newState) return newState; }); } else { /* istanbul ignore next */ console.warn( 'action', action, 'need to be a Function which map from current state to new state' ); } }, () => { this.context[XREACT_ENGINE].history$.complete(this.state) if (process.env.NODE_ENV == 'production') { console.error('YOU HAVE TERMINATED THE INTENT STREAM...') } if (process.env.NODE_ENV == 'debug') { console.log(`LAST STATE is`, this.state) } } ); } componentWillUnmount() { this.subscription.unsubscribe(); } render() { if (isSFC(WrappedComponent)) { return h( WrappedComponent, Object.assign({}, opts, this.props, this.state, { actions: this.machine.actions, }) ); } else { return h( WrappedComponent, Object.assign({}, opts, this.props, this.state, { actions: this.machine.actions, }) ); } } } } function getDisplayName(WrappedComponent: React.ComponentType) { return WrappedComponent.displayName || WrappedComponent.name || 'X'; } function bindActions(actions: Actions, intent$: Subject, self: XcomponentClass | Xcomponent) { let _actions: Actions = { fromEvent(e: Event) { return intent$.next(e); }, fromPromise(p: Promise) { return p.then(x => intent$.next(x)); }, terminate(a: I) { if (process.env.NODE_ENV == 'debug') console.error('INTENT TERMINATED') return intent$.complete(a) } }; for (let a in actions) { _actions[a] = (...args: any[]) => { return intent$.next(actions[a].apply(self, args)); }; } return _actions; } function pick(names: Array, obj: A) { let result = >{}; for (let name of names) { if (obj[name]) result[name] = obj[name]; } return result; } function isPromise(p: any): p is Promise { return p !== null && typeof p === 'object' && typeof p.then === 'function' }