UNPKG

valuelink

Version:

Purely functional two-way data binding and form validation for React

333 lines (273 loc) 9.77 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 { StateRef as Link } // Main Link class. All links must extend it. export abstract class StateRef< T >{ // Create custom link to arbitrary value static value< T >( value : T, set : ( x : T ) => void ) : StateRef< T >{ return new CustomStateRef( value, set ); } /** * Unwrap object with links, returning an object of a similar shape filled with link values. */ static getValues<K extends keyof L, L extends RefsHash>( links : L ) : { [ name in K ] : any } { return unwrap( links, 'value' ) as any; } // EXPERIMENTAL: Support useRef interface. get current(){ return this.value; } set current( x : T ){ this.set( x ); } // Private accessor for whenChanged. Uniform with Type-R models and collections API. private get _changeToken(){ return this.value; } /** * Unwrap object with links, returning an object of a similar shape filled with link errors. */ static getErrors<K extends keyof L, L extends RefsHash>( links : L ) : { [ name in K ] : L[name]["value"] } { return unwrap( links, 'error' ) as any; } /** * Return true if an object with links contains any errors. */ static hasErrors<L extends RefsHash>( links : L ) : boolean { for( let key in links ){ if( links.hasOwnProperty( key ) && links[ key ].error ){ return true; } } return false; } /** * Assing links with values from the source object. */ static setValues( links : RefsHash, source : object ) : void { if( source ){ for( let key in links ){ const sourceKey = trim( key ); if( source.hasOwnProperty( sourceKey ) ){ const sourceVal = source[ sourceKey ]; sourceVal === void 0 || links[ key ].set( sourceVal ); } } } } constructor( public value : T ){} // Validation error. Usually is a string with error text, but can hold any type. error : any // Link set functions abstract set( x : T ) : void onChange( handler : ( x : T ) => void ) : StateRef< T > { return new ClonedStateRef( 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 ) }; } // 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 > ) : StateRef< T > { return new ClonedStateRef( 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 ) : StateRef<boolean> { return new EqualsRef( this, truthyValue ); } enabled( defaultValue? : T ) : StateRef<boolean> { return new EnabledRef( this, defaultValue || "" ); } // Array-only links methods contains<E>( this : StateRef<E[]>, element : E ) : StateRef<boolean>{ return new ContainsRef( this, element ); } push<E>( this : StateRef<E[]>, ...args : E[] ) : void; push(){ const array = arrayHelpers.clone( this.value ); Array.prototype.push.apply( array, arguments ); this.set( array ); } unshift<E>( this : StateRef<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 : StateRef<E[]>, iterator : ( link : RefAt<E, number>, idx : number ) => Z ) : Z[]; map<E, Z>( this : StateRef<{[ key : string ] : E }>, iterator : ( link : RefAt<E, string>, idx : string ) => Z ) : Z[]; map( iterator ) { return helpers( this.value ).map( this, iterator ); } removeAt<E>( this : StateRef<E[]>, key : number ) : void; removeAt<E>( this : StateRef<{ [ key : string ] : E }>, key : string ) : void; removeAt( key ){ const { value } = this, _ = helpers( value ); this.set( _.remove( _.clone( value ), key ) ); } at< E >( this : StateRef< E[] >, key : number ) : RefAt<E, number>; at< K extends keyof T, E extends T[K]>( key : K ) : RefAt<E, K>; at( key ){ return new RefAt( this, key ); } clone() : T { let { value } = this; return helpers( value ).clone( value ); } /** * Convert link to object to the object of links. Optionally filter by */ pick< K extends keyof T >( ...keys : K[]) : {[ P in K ]: StateRef<T[P]>} pick() { let links = {}, keys = arguments.length ? arguments : Object.keys( this.value ); for( let i = 0; i < keys.length; i++ ){ const key : string = keys[ i ]; links[ key ] = new RefAt( this, key ); } return links; } /** * Convert link to object to the object of links with $-keys. */ $links() : { [ key : string ] : StateRef<any> }{ let links : RefsHash = {}, { value } = this; for( let key in value ){ if( value.hasOwnProperty( key ) ){ links[ '$' + key ] = new RefAt( this, key ); } } return links as any; } /** * 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 CustomStateRef< T > extends StateRef< T > { set( x ){} constructor( value : T, set : ( x : T ) => void ){ super( value ); this.set = set; } } export class ClonedStateRef< T > extends StateRef< T > { set( x ){} constructor( parent : StateRef< T >, set : ( x : T ) => void ){ super( parent.value ); this.set = set; const { error } = parent; if( error ) this.error = error; } } export class EqualsRef extends StateRef< boolean > { constructor( public parent : StateRef< any >, public truthyValue ){ super( parent.value === truthyValue ); } set( x : boolean ) : void { this.parent.set( x ? this.truthyValue : null ); } } export class EnabledRef extends StateRef< boolean > { constructor( public parent : StateRef< any >, public defaultValue ){ super( parent.value != null ); } set( x : boolean ){ this.parent.set( x ? this.defaultValue : null ); } } export class ContainsRef extends StateRef< boolean > { constructor( public parent : StateRef< 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 RefAt< E, K > extends StateRef< E > { constructor( private parent : StateRef< 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; } ); } }; } export interface RefsHash { [ name : string ] : StateRef<any> } function unwrap( links : RefsHash, field : string) : object { const values = {}; for( let key in links ){ if( links.hasOwnProperty( key ) ){ const value = links[ key ][ field ]; if( value !== void 0 ){ values[ trim( key ) ] = value; } } } return values; } function trim( key : string ){ return key[ 0 ] === '$' ? key.slice( 1 ) : key; }