transactional
Version:
Reactive objects with transactional updates and automatic serialization
188 lines (147 loc) • 5.83 kB
text/typescript
import { setAttribute, Record, Attribute, Transform, ChangeHandler, AttributeDescriptor } from './transaction.ts'
import { notEqual, assign } from '../tools.ts'
import { Owner, Transactional, TransactionOptions, Constructor } from '../types.ts'
type GetHook = ( value : any, key : string ) => any;
export type ChangeAttrHandler = ( ( value : any, attr : string ) => void ) | string;
declare global {
interface Function {
_attribute : typeof GenericAttribute
}
}
// TODO: interface differs from options, do something obout it
export class GenericAttribute implements Attribute {
// Factory method to create attribute from options
static create( options : AttributeDescriptor, name : string ) : GenericAttribute {
const type = options.type,
AttributeCtor = type ? type._attribute : GenericAttribute;
return new AttributeCtor( name, options );
}
/**
* Update pipeline functions
* =========================
*
* Stage 0. canBeUpdated( value )
* - presence of this function implies attribute's ability to update in place.
*/
canBeUpdated( prev, next ) : boolean {
return false;
}
/**
* Stage 1. Transform stage
*/
transform( value, options, prev, model ) { return value; }
// convert attribute type to `this.type`
convert( value, options, model ) { return value; }
/**
* Stage 2. Check if attr value is changed
*/
isChanged( a, b ) {
return notEqual( a, b );
}
/**
* Stage 3. Handle attribute change
*/
handleChange( next, prev, model ) {}
/**
* End update pipeline definitions.
*/
// create empty object passing backbone options to constructor...
create() { return new ( <any>this.type )(); }
// generic clone function for typeless attributes
// Must be overriden in sublass
clone( value, options : { deep? : boolean } = {} ) {
if( value && typeof value === 'object' ) {
// delegate to object's clone(), if it exist...
if( value.clone ) {
return value.clone( options );
}
if( options.deep ){
const proto = Object.getPrototypeOf( value );
// attempt to deep copy raw objects, assuming they are JSON
if( proto === Object.prototype || proto === Array.prototype ){
return JSON.parse( JSON.stringify( value ) );
}
}
}
return value;
}
validate( record : Record, value : any, key : string ){}
toJSON( value, key ) {
return value && value.toJSON ? value.toJSON() : value;
}
createPropertyDescriptor() : PropertyDescriptor | void {
const { name, getHook } = this;
if( name !== 'id' ){
return {
// call to optimized set function for single argument.
set( value ){
setAttribute( <any>this, name, value );
},
// attach get hook to the getter function, if present
get : getHook ?
function() {
return getHook.call( this, this.attributes[ name ], name );
} :
function() {
return this.attributes[ name ];
}
}
}
}
value : any
type : Constructor
parse : ( value, key : string ) => any
initialize( name : string, options ){}
constructor( public name : string, public options : AttributeDescriptor ) {
const {
value, type, parse, toJSON,
getHooks = [],
transforms = [],
changeHandlers = []
} = options;
this.value = value;
this.type = type;
this.parse = parse;
this.toJSON = toJSON === void 0 ? this.toJSON : toJSON;
/**
* Assemble pipelines...
*/
// `convert` is default transform, which is always present...
this.transform = this.convert;
// No change handler by default
this.handleChange = null;
// Get hook from the attribute will be used first...
this.getHook = this.get || null;
// let subclasses configure the pipeline...
this.initialize.apply( this, arguments );
// let attribute spec configure the pipeline...
getHooks.forEach( gh => this.addGetHook( gh ) );
transforms.forEach( t => this.addTransform( t ) );
changeHandlers.forEach( ch => this.addChangeHandler( ch ) );
}
getHook : ( value, key : string ) => any
get : ( value, key : string ) => any
addGetHook( next : GetHook ) : void {
const prev = this.getHook;
this.getHook = prev ?
function( value, name ) {
const next = prev.call( value, name );
return next.call( next, name );
} : next;
}
addTransform( next : Transform ) : void {
const prev = this.transform;
this.transform = function( value, options, prev, model ) {
const next = prev.call( this, value, options, prev, model );
return next.call( this, next, options, prev, model );
}
}
addChangeHandler( next : ChangeHandler ) : void {
const prev = this.handleChange;
this.handleChange = prev ?
function( next, prev, model ) {
prev.call( this, next, prev, model );
next.call( this, next, prev, model );
} : next;
}
}