import * as React from 'react' ; import { structEq } from './../utils' ; import { Atom } from './../atom' ; import { warning , getReactComponentName , DEV_ENV } from './../utils' ; import { Observable , ObservableInput , Subscription as RxSubscription , combineLatest , of } from 'rxjs' ; import { scan , map } from 'rxjs/operators' ; export interface Subscription { unsubscribe ( ) : void ; } export type Lifted < T > = { [ K in keyof T ] : T [ K ] | Observable < T [ K ] > ; } ; export interface LiftWrapperProps < TProps > { component : React . Component < TProps , any > | React . StatelessComponent < TProps > | React . ComponentClass < TProps > | React . ComponentType | keyof React . ReactHTML ; props : Lifted < TProps > ; } export interface LiftWrapperState { renderCache ? : React . DOMElement < any , any > | null ; subscription ? : Subscription | null ; } /**
 * A wrapper component that allows to use observables for prop values of a
 * given component.
 */ export class LiftWrapper < TProps > extends React . Component < LiftWrapperProps < TProps > , LiftWrapperState > { state = LiftWrapper . _initState ; static _initState : LiftWrapperState = { renderCache : null , subscription : null } ; static _endState : LiftWrapperState = { subscription : null } ; render ( ) { return this . state . renderCache || null ; } private _subscribe ( newProps ) { const { props , component } = newProps ; let n = 0 ; walkObservables ( props , ( ) => n += 1 ) ; switch ( n ) { case 0 : this . setState ( { subscription : null , renderCache : render ( component , props ) } ) ; break ; case 1 : new RenderOne ( this , newProps ) ; break ; default : new RenderMany ( this , newProps , n ) ; break ; } } private _unsubscribe ( ) { const subscription = this . state ? this . state . subscription : null ; if ( subscription ) subscription . unsubscribe ( ) ; } UNSAFE_componentWillReceiveProps ( newProps ) { this . _unsubscribe ( ) ; this . _subscribe ( newProps ) ; } UNSAFE_componentWillMount ( ) { this . _unsubscribe ( ) ; this . _subscribe ( this . props ) ; } componentWillUnmount ( ) { this . _unsubscribe ( ) ; this . setState ( LiftWrapper . _initState ) ; } shouldComponentUpdate ( _newProps , newState , _newContext ) { return newState . renderCache !== this . state . renderCache ; } } export type LiftedComponentProps < TProps > = Lifted < TProps > & { mount ? : React . Ref < HTMLElement > ; } ; example export function lift < TProps > ( component ) { return ( props ) => React . createElement < LiftWrapperProps < TProps > > ( LiftWrapper , { component : component , props : props } ) ; } const PROP_CHILDREN = 'children' ; const PROP_STYLE = 'style' ; const PROP_MOUNT = 'mount' ; const PROP_FORWARD_REF = 'forwardRef' ; const PROP_REF = 'ref' ; param props param action function walkObservables < T > ( props , action ) { for ( const key in props ) { const value = props [ key ] ; if ( value instanceof Observable ) { action ( value ) ; } else if ( PROP_CHILDREN === key && value instanceof Array ) { const n = value . length ; for ( let i = 0 ; i < n ; ++ i ) { const child = value [ i ] ; if ( child instanceof Observable ) action ( child ) ; } } else if ( PROP_STYLE === key ) { for ( const k in value ) { const style = value [ k ] ; if ( style instanceof Observable ) action ( style ) ; } } } } param class_ param props param values returns function render < P > ( class_ , props , observedValues = [ ] ) { const newProps = { } ; let newChildren ; let k = - 1 ; for ( const key in props ) { const propValue = ( props as any ) [ key ] ; const isChildren = key === PROP_CHILDREN ; const isForwardRef = key === PROP_FORWARD_REF || key === PROP_MOUNT ; const isStyle = key === PROP_STYLE ; if ( propValue instanceof Observable ) { const observedValue = observedValues [ ++ k ] ; if ( isChildren ) { newChildren = observedValue ; } else if ( isForwardRef ) { newProps . ref = observedValue ; } else { newProps [ key ] = observedValue ; } } else if ( isChildren ) { if ( propValue instanceof Array ) { const n = propValue . length ; for ( let i = 0 ; i < n ; ++ i ) { const child = propValue [ i ] ; if ( child instanceof Observable ) { if ( ! newChildren ) { newChildren = Array ( propValue . length ) ; for ( let j = 0 ; j < i ; ++ j ) newChildren [ j ] = propValue [ j ] ; } const childValue = observedValues [ ++ k ] ; newChildren [ i ] = ! childValue || childValue . key ? childValue : React . createElement ( React . Fragment , { key : i } , childValue ) ; } else if ( newChildren ) { newChildren [ i ] = propValue [ i ] ; } } } newChildren = newChildren || propValue ; } else if ( isForwardRef ) { newProps . ref = propValue ; } else if ( isStyle ) { let newStyle ; for ( const i in propValue ) { const style = propValue [ i ] ; if ( style instanceof Observable ) { if ( ! newStyle ) { newStyle = { } ; for ( const j in propValue ) { if ( j === i ) break ; newStyle [ j ] = propValue [ j ] ; } } newStyle [ i ] = observedValues [ ++ k ] ; } else if ( newStyle ) { newStyle [ i ] = style ; } } newProps . style = newStyle || propValue ; } else { newProps [ key ] = propValue ; } } return React . createElement ( class_ as any , newProps , newChildren === undefined ? null : newChildren ) ; } /**
 * A dummy React component mock that is used to preserve any state changes
 * pushed onto the component.
 */ class FakeComponent < P > { constructor ( public state , public props ) { } setState ( state ) { const newState = typeof state === 'function' ? state ( this . state ) : state ; if ( 'subscription' in newState ) this . state . subscription = newState . subscription ; if ( 'renderCache' in newState ) this . state . renderCache = newState . renderCache ; } } const handleError = ( e ) => { throw e ; } ; function warnEmptyObservable ( componentName ) { warning ( `${ componentName ? `The component <${ componentName }>` : 'An unnamed component' } has ` + `received an observable that doesn't immediately emit a value in one of its props. ` + `Since this observable hasn't yet called its subscription handler, the component ` + `can not be rendered at the time. ` + `Check the props of ${ componentName ? `<${ componentName }>` : 'this component' }.` ) ; } template P class RenderOne < P > implements Subscription { private _liftedComponent : LiftWrapper < P > ; private _innerSubscription : RxSubscription | null = null ; private _receivedValue = false ; constructor ( liftedComponent , newProps ) { const state = { subscription : this , renderCache : liftedComponent . state && liftedComponent . state . renderCache } ; this . _liftedComponent = new FakeComponent < P > ( state , newProps ) as LiftWrapper < P > ; walkObservables ( newProps . props , observable => { this . _innerSubscription = observable . subscribe ( ( v ) => this . _handleValue ( v ) , handleError , ( ) => this . _handleCompleted ( ) ) ; if ( this . _innerSubscription && this . _innerSubscription . closed ) this . _innerSubscription = null ; } ) ; if ( DEV_ENV && ! this . _receivedValue ) warnEmptyObservable ( getReactComponentName ( this . _liftedComponent . props . component ) ) ; this . _liftedComponent = liftedComponent ; liftedComponent . setState ( state ) ; } unsubscribe ( ) { if ( this . _innerSubscription ) this . _innerSubscription . unsubscribe ( ) ; } private _handleValue ( value ) { if ( DEV_ENV ) this . _receivedValue = true ; const liftedComponent = this . _liftedComponent ; const { component , props } = liftedComponent . props ; const renderCache = render ( component , props , [ value ] ) ; liftedComponent . setState ( state => ! structEq ( state . renderCache , renderCache ) ? { renderCache } : { } ) ; } private _handleCompleted ( ) { this . _innerSubscription = null ; this . _liftedComponent . setState ( LiftWrapper . _endState ) ; } } template P class RenderMany < P > implements Subscription { private _liftedComponent : LiftWrapper < P > ; private _values : any [ ] ; private _innerSubscriptions : ( RxSubscription | null ) [ ] ; constructor ( liftedComponent , newProps , N ) { const state = { subscription : this , renderCache : liftedComponent . state && liftedComponent . state . renderCache } ; this . _liftedComponent = new FakeComponent ( state , newProps ) as LiftWrapper < P > ; this . _innerSubscriptions = [ ] ; this . _values = Array ( N ) ; for ( let i = 0 ; i < N ; ++ i ) this . _values [ i ] = this ; walkObservables ( newProps . props , observable => { const i = this . _innerSubscriptions . length ; let subscription = observable . subscribe ( ( v ) => this . _handleValue ( i , v ) , handleError , ( ) => this . _handleCompleted ( i ) ) ; if ( subscription && subscription . closed ) subscription = null ; this . _innerSubscriptions . push ( subscription ) ; } ) ; if ( DEV_ENV ) for ( let i = this . _values . length - 1 ; 0 <= i ; -- i ) if ( this . _values [ i ] === this ) { warnEmptyObservable ( getReactComponentName ( liftedComponent . props . component ) ) ; break ; } this . _liftedComponent = liftedComponent ; liftedComponent . setState ( state ) ; } unsubscribe ( ) { let i = - 1 ; walkObservables ( this . _liftedComponent . props . props , _ => { const unsubscriber = this . _innerSubscriptions [ ++ i ] ; if ( unsubscriber ) unsubscriber . unsubscribe ( ) ; } ) ; } private _handleValue ( idx , value ) { this . _values [ idx ] = value ; for ( let i = this . _values . length - 1 ; 0 <= i ; -- i ) if ( this . _values [ i ] === this ) return ; const liftedComponent = this . _liftedComponent ; const { component , props } = liftedComponent . props ; const renderCache = render ( component , props , this . _values ) ; liftedComponent . setState ( state => ! structEq ( state . renderCache , renderCache ) ? { renderCache } : { } ) ; } private _handleCompleted ( idx ) { const n = this . _innerSubscriptions . length ; if ( n > idx ) this . _innerSubscriptions [ idx ] = null ; if ( n !== this . _values . length ) return ; for ( let i = 0 ; i < n ; ++ i ) if ( this . _innerSubscriptions [ i ] ) return ; this . _liftedComponent . setState ( LiftWrapper . _endState ) ; } } export type ObservableInputLike < T > = T | ObservableInput < T > ; export type ObservableLike < T > = T | Observable < T > ; export type ClassNameLike = undefined | null | boolean | string ; /**
 * Filter out undefined, null, false and empty strings.
 * Throw on a `true` value.
 */ function filterClassNames ( cs ) { return cs . filter ( c => { if ( c === true ) throw new TypeError ( 'Unexpected `true` value in classes' ) ; return c !== null && c !== undefined && c !== '' && c !== false ; } ) as ( string | ObservableInput < ClassNameLike > ) [ ] ; } export function classes ( ... cs ) ; export function classes ( ... cs ) ; /**
 * Declare element's classes through an array of observables.
 *
 * Each observable defines the presence of a single respective CSS
 * class in the resulting element's class attribute. If this observable's
 * most recent value is a null or undefined, it will not be included in
 * the resulting class attribute value.
 *
 * You can also use this with regular components, but if you have *at least one*
 * observable in the class name list, you will *need* to use a
 * [lifted component] LiftedComponent only.
 *
 *  example example returns export function classes ( ... cs ) { if ( ! cs || cs . find ( x => x instanceof Observable ) === undefined ) { const filtered = filterClassNames ( ( cs || [ ] ) as ClassNameLike [ ] ) as string [ ] ; return { className : filtered . length > 0 ? filtered . join ( ' ' ) : undefined } ; } else { return { className : combineLatest ( filterClassNames ( cs || [ ] ) . map ( x => ! ( x instanceof Observable ) ? of ( x ) : x ) , ( ... cs ) => { const filtered = filterClassNames ( cs || [ ] ) ; return filtered . length > 0 ? filtered . join ( ' ' ) : undefined ; } ) } ; } } function combineTemplate ( template ) { const keys = [ ] ; const values = [ ] ; for ( const k in template ) { keys . push ( k ) ; values . push ( template [ k ] ) ; } return combineLatest ( values , ( ... vs ) => { const r = { } ; for ( let i = 0 ; i < keys . length ; i ++ ) { r [ keys [ i ] ] = vs [ i ] ; } return r ; } ) ; } /**
 * Creates a callback to be used with a React [ref] React . ClassAttributes * prop to propagate any values in given observable dictionary to
 * respective instance element properties.
  export function setElementProps < TElement extends Element > ( template ) { let observable = null ; let subscription = null ; return ( domElement ) => { if ( subscription ) { subscription . unsubscribe ( ) ; subscription = null ; observable = null ; } if ( domElement ) { observable = combineTemplate ( template ) ; subscription = observable . subscribe ( value => { for ( const k in value ) ( domElement as any ) [ k ] = value [ k ] ; } , handleError , ( ) => { observable = null ; subscription = null ; } ) ; } } ; } /**
 * Creates a React event handler that will set the given dictionary of
 * atoms to their respective component property values.
 */ export function getElementProps ( template ) { return ( e ) => { for ( const k in template ) { template [ k ] . set ( ( e . target as any ) [ k ] ) ; } } ; } type BindElementPropsReturnType = { [ x ] : ( ( e : React . SyntheticEvent < any > ) => void ) | ( ( domElement : Element | null ) => void ) ; [ PROP_REF ] ( domElement ) : void ; } | { [ x ] : ( ( e : React . SyntheticEvent < any > ) => void ) | ( ( domElement : Element | null ) => void ) ; [ PROP_MOUNT ] ( domElement ) : void ; } | { [ x ] : ( ( e : React . SyntheticEvent < any > ) => void ) | ( ( domElement : Element | null ) => void ) ; [ PROP_FORWARD_REF ] ( domElement ) : void ; } | { } ; export function bindElementProps ( template ) { const { [ PROP_REF ] : ref , [ PROP_MOUNT ] : mount , [ PROP_FORWARD_REF ] : forwardRef , ... tpl } = template ; const elementRef = setElementProps ( tpl ) ; const elementPropsHandler = getElementProps ( tpl as { [ k ] : Atom < any > ; } ) ; return ref ? ( { [ PROP_REF ] : elementRef , [ ref ] : elementPropsHandler } ) : forwardRef ? ( { [ PROP_FORWARD_REF ] : elementRef , [ forwardRef ] : elementPropsHandler } ) : mount ? ( { [ PROP_MOUNT ] : elementRef , [ mount ] : elementPropsHandler } ) : { } ; } /**
 * Two-way bind component props to respective atoms. Any changes in the
 * atom will then trigger component update, and any change in the component's
 * prop value will be pushed onto the atom.
 *
 * The component is _required_ to be a  LiftedComponent .
 *
 * Your component needs to be lifted.
 *
 *  example returns export function bind ( template ) { return { ... template , ... { onChange : getElementProps ( template ) } } ; } export function reactiveList < TValue > ( ids , createListItem ) ; export function reactiveList < TValue > ( ids , createListItem ) ; /**
 * Derive a reactive list from:
 * - an observable of list item ids
 * - a list item factory – a function that will create a list item based on item id.
 */ export function reactiveList < TValue , TKey extends string | number > ( ids , createListItem ) { return ids . pipe ( scan < TKey [ ] , [ { [ k ] : TValue ; } , TValue [ ] ] > ( ( [ oldIds , _ ] , ids ) => { const newIds = { } ; const newValues = Array ( ids . length ) ; const n = ids . length ; for ( let i = 0 ; i < n ; ++ i ) { const id = ids [ i ] ; const k = id . toString ( ) ; if ( k in newIds ) { newValues [ i ] = newIds [ k ] ; } else { newIds [ k ] = newValues [ i ] = k in oldIds ? oldIds [ k ] : ( createListItem as ( _ : string | number ) => TValue ) ( id ) ; } } return [ newIds , newValues ] ; } , [ { } , [ ] ] ) , map ( ( [ , values ] ) => values ) ) ; }