UNPKG

nestedreact

Version:

Advanced models, state management, and data binding solution for React

252 lines (204 loc) 7.55 kB
/** * Advanced React links for purely functional two-way data binding * * MIT License, (c) 2016 Vlad Balin, Volicon. */ import { helpers, arrayHelpers, objectHelpers } from './helpers' export type Transform< T > = ( value : T, event? : {} ) => T export type EventHandler = ( event : {} ) => void export interface Validator< T >{ ( value : T ) : boolean error? : any } export type LinksCache< S, X extends keyof S> = { [ K in X ] : Link< S[ K ] > } // Main Link class. All links must extend it. export abstract class Link< T >{ // @deprecated API. Use component subclass. static state : < P, S, K extends keyof S>( component : React.Component< P, S >, key : K ) => Link< S[ K ] >; static all : < P, S, K extends keyof S >( component : React.Component< P, S >, ..._keys : K[] ) => LinksCache< S, K >; // Create custom link to arbitrary value static value< T >( value : T, set : ( x : T ) => void ) : Link< T >{ return new CustomLink( value, set ); } // create constructor( public value : T ){} // Validation error. Usually is a string with error text, but can hold any type. error : any // DEPRECATED: Old error holder for backward compatibility with Volicon code base get validationError() : any { return this.error } // Link set functions abstract set( x : T ) : void onChange( handler : ( x : T ) => void ) : Link< T > { return new CloneLink( this, (x : T ) => { handler( x ); this.set( x ); }); } // <input { ...link.props } /> get props(){ return typeof this.value === 'boolean' ? { checked : this.value, onChange : e => this.set( Boolean( e.target.checked ) as any ) }:{ value : this.value, onChange : e => this.set( e.target.value ) }; } // DEPRECATED: Old React method for backward compatibility requestChange( x : T ) : void { this.set( x ); } // Immediately update the link value using given transform function. update( transform : Transform< T >, e? : Object ) : void { const next = transform( this.clone(), e ); next === void 0 || this.set( next ); } // Create new link which applies transform function on set. pipe( handler : Transform< T > ) : Link< T > { return new CloneLink( this, x =>{ const next = handler( x, this.value ); next === void 0 || this.set( next ); } ); } // Create UI event handler function which will update the link with a given transform function. action( transform : Transform< T > ) : EventHandler { return e => this.update( transform, e ); } equals( truthyValue : T ) : Link<boolean> { return new EqualsLink( this, truthyValue ); } enabled( defaultValue? : T ) : Link<boolean> { return new EnabledLink( this, defaultValue || "" ); } // Array-only links methods contains<E>( this : Link<E[]>, element : E ) : Link<boolean>{ return new ContainsLink( this, element ); } push<E>( this : Link<E[]>, ...args : E[] ) : void; push(){ const array = arrayHelpers.clone( this.value ); Array.prototype.push.apply( array, arguments ); this.set( array ); } unshift<E>( this : Link<E[]>, ...args : E[] ) : void; unshift() : void { const array = arrayHelpers.clone( this.value ); Array.prototype.unshift.apply( array, arguments ); this.set( array ); } splice( start : number, deleteCount? : number ); splice() : void { const array = arrayHelpers.clone( this.value ); Array.prototype.splice.apply( array, arguments ); this.set( array ); } // Array and objects universal collection methods map<E, Z>( this : Link<E[]>, iterator : ( link : LinkAt<E, number>, idx : number ) => Z ) : Z[]; map<E, Z>( this : Link<{[ key : string ] : E }>, iterator : ( link : LinkAt<E, string>, idx : string ) => Z ) : Z[]; map( iterator ) { return helpers( this.value ).map( this, iterator ); } removeAt<E>( this : Link<E[]>, key : number ) : void; removeAt<E>( this : Link<{ [ key : string ] : E }>, key : string ) : void; removeAt( key ){ const { value } = this, _ = helpers( value ); this.set( _.remove( _.clone( value ), key ) ); } at< E >( this : Link< E[] >, key : number ) : LinkAt<E, number>; at< K extends keyof T, E extends T[K]>( key : K ) : LinkAt<E, K>; at( key ){ return new LinkAt( this, key ); } clone() : T { let { value } = this; return helpers( value ).clone( value ); } pick< K extends keyof T >( ...keys : K[]) : {[ P in K ]: Link<T[P]>} pick() { let links = {}; for( let i = 0; i < arguments.length; i++ ){ const key : string = arguments[ i ]; links[ key ] = new LinkAt( this, key ); } return links; } /** * Validate link with validness predicate and optional custom error object. Can be chained. */ check( whenValid : Validator< T >, error? : any ) : this { if( !this.error && !whenValid( this.value ) ){ this.error = error || whenValid.error || defaultError; } return this; } } export class CustomLink< T > extends Link< T > { set( x ){} constructor( value : T, set : ( x : T ) => void ){ super( value ); this.set = set; } } export class CloneLink< T > extends Link< T > { set( x ){} constructor( parent : Link< T >, set : ( x : T ) => void ){ super( parent.value ); this.set = set; const { error } = parent; if( error ) this.error = error; } } export class EqualsLink extends Link< boolean > { constructor( public parent : Link< any >, public truthyValue ){ super( parent.value === truthyValue ); } set( x : boolean ) : void { this.parent.set( x ? this.truthyValue : null ); } } export class EnabledLink extends Link< boolean > { constructor( public parent : Link< any >, public defaultValue ){ super( parent.value != null ); } set( x : boolean ){ this.parent.set( x ? this.defaultValue : null ); } } export class ContainsLink extends Link< boolean > { constructor( public parent : Link< any >, public element : any ){ super( parent.value.indexOf( element ) >= 0 ); } set( x : boolean ){ var next = Boolean( x ); if( this.value !== next ){ var arr : any[] = this.parent.value, nextValue = x ? arr.concat( this.element ) : arr.filter( el => el !== this.element ); this.parent.set( nextValue ); } } } const defaultError = 'Invalid value'; /** * Link to array or object element enclosed in parent link. * Performs purely functional update of the parent, shallow copying its value on `set`. */ export class LinkAt< E, K > extends Link< E > { constructor( private parent : Link< any >, public key : K ){ super( parent.value[ key ] ); } remove(){ this.parent.removeAt( <any>this.key ); } // Set new element value to parent array or object, performing purely functional update. set( x : E ) : void { if( this.value !== x ){ this.parent.update( value => { value[ this.key ] = x; return value; } ); } }; }