@pilotlab/data
Version:
A luxurious user experience framework, developed by your friends at Pilot.
395 lines (312 loc) • 15.3 kB
text/typescript
import is from '@pilotlab/is';
import { List } from '@pilotlab/collections';
import { Signal } from '@pilotlab/signals';
import { Identifier } from '@pilotlab/ids';
import { IPromise, Result } from '@pilotlab/result';
import { NodeType, NodeChangeType, NodeBase } from '@pilotlab/nodes';
import { AttributeChangeActions, AttributesWrapType, DataType } from './attributeEnums';
import {
IAnimationBatch,
} from '@pilotlab/animation';
import DataTools from '../dataTools';
import AttributesBase from './attributesBase';
import AttributeSetReturn from './attributeSetReturn';
import AttributeChangeOptions from './attributeChangeOptions';
import IAttributes from './interfaces/iAttributes';
import IAttributeFactory from './interfaces/iAttributeFactory';
import IAttributeChangeOptions from './interfaces/iAttributeChangeOptions';
import IAttributeSetReturn from './interfaces/iAttributeSetReturn';
import IAttributeUpdateTracker from './interfaces/iAttributeUpdateTracker';
import AttributeEventArgs from './attributeEventArgs';
import Attributes from './attributes';
import AttributeRoot from './attributeRoot';
import {NodeEventArgs} from "../../../nodes/src/nodeEventArgs";
import INodeChangeOptions from "../../../nodes/src/interfaces/iNodeChangeOptions";
import Debug from "../../../debug";
import INode from "../../../nodes/src/interfaces/iNode";
export abstract class AttributeBase<
TValue,
TNode extends AttributeBase<any, any, any, any>,
TNodes extends Attributes,
TRoot extends AttributeRoot>
extends NodeBase<TNode, TNodes, TRoot> {
protected constructor(
factory:IAttributeFactory,
value?:any,
dataType?:(DataType | string),
label?:string,
key:string = '',
nodeType:NodeType = NodeType.BASIC,
isInitialize:boolean = true
) {
super(factory, nodeType, key, false);
this.p_value = null;
if (is.notEmpty(label, true)) this.p_label = label;
this.p_dataType = DataType.UNDEFINED;
this.p_changed = new Signal<AttributeEventArgs<TNode>>(false);
this.p_isSignalChange = true;
this.p_isAllowSave = true;
this.p_isAllowAutoPropterties = true;
this.p_returnValue = new AttributeSetReturn<TNode>(<TNode><AttributeBase<any, any, any, any>>this, false);
this.p_formatter = (value:any) => value;
if (isInitialize) this.initialize(value, dataType, nodeType);
}
protected p_onInitializeStarted(result:Result<any>, args:any[]):IPromise<any> {
const value:any = args[0];
const dataType:(DataType | string) = args[1];
const nodeType:NodeType = args[2];
super.p_onInitializeStarted(new Result<any>(), [nodeType]).then(() => {
/// Set the data type first, then the value
if (is.notEmpty(dataType)) this.p_dataType = dataType;
else {
if (is.notEmpty(value, false)) this.p_dataType = DataTools.getDataType(value);
else this.p_dataType = DataType.OBJECT;
}
if (this.create.getBaseDataType(this.p_dataType) === DataType.COLLECTION) {
this.p_wrapAttributes(value, AttributesWrapType.CLEAR_BASE);
} else if (is.notEmpty(value, false)) this.p_value = value;
result.resolve(value);
});
return result;
}
/**
* All data associated with this entity will be saved to the provided attribute collection,
* and all change events will be propagated through that collection.
*/
protected p_wrapAttributes(value?:any, wrapType:AttributesWrapType = AttributesWrapType.OVERWRITE_BASE):void {
let changeOptions:IAttributeChangeOptions = AttributeChangeOptions.noSaveOrSignal;
let attributes:IAttributes;
/// If an Attributes instance was passed as the value, it will be directly assigned to this.p_value.
if (is.notEmpty(value)) { attributes = this.create.collection.fromAny(value, this); }
if (is.empty(attributes)) attributes = this.create.collection.instance(this);
this.p_value = attributes;
if (is.notEmpty(this.p_value)) {
if (wrapType === AttributesWrapType.CLEAR_BASE) {
/// Do nothing.
} else if (wrapType === AttributesWrapType.CLEAR_WRAPPED) {
attributes.clear(changeOptions);
attributes.update(this.p_value, changeOptions);
} else if (wrapType === AttributesWrapType.OVERWRITE_BASE) {
this.p_value.update(attributes, changeOptions);
attributes.clear(AttributeChangeOptions.noSaveOrSignal);
attributes.update(this.p_value, changeOptions);
} else if (wrapType === AttributesWrapType.OVERWRITE_WRAPPED) {
attributes.update(this.p_value, changeOptions);
}
}
}
/*====================================================================*
START: Factory
*====================================================================*/
get create():IAttributeFactory { return this.p_factory; }
protected p_factory:IAttributeFactory;
/*====================================================================*
START: Properties
*====================================================================*/
protected p_returnValue:IAttributeSetReturn;
/**
* A UI-friendly title for the attribute that can be presented to users.
* If no label is set, the key of the node will be returned.
*/
get label():string { return is.notEmpty(this.p_label, true) ? this.p_label : <string>this.key; }
set label(value:string) { this.p_label = value; }
protected p_label:string;
/**
* A brief description of that attribute that can be used to supply info to the user
* about how the attribute should be used.
*/
get summary():string { return is.notEmpty(this.p_summary, true) ? this.p_summary : ''; }
set summary(value:string) { this.p_summary = value; }
protected p_summary:string;
/**
* The value of the associated data.
* This property cannot be set directly. You must use the set() method to change the value.
* This allows attributes to be wrapped in an AttributeReader instance, which effectively
* renders them immutable.
*/
get value():TValue { return <TValue>this.p_value; }
set value(value:TValue) { this.set(value, this.isSignalChange ? AttributeChangeOptions.save : AttributeChangeOptions.zero); }
get formatted():TValue {
let value:TValue = <TValue>this.p_value;
value = this.p_format(value);
return value;
}
protected p_value:any;
protected p_valueTarget:any; /// Used for animation.
get isCollection():boolean {
return (
this.nodeType === NodeType.ROOT ||
this.nodeType === NodeType.COLLECTION ||
this.create.getBaseDataType(this.dataType) === DataType.COLLECTION
);
}
/// This property will hold dynamically generated getters and setters
/// for each child attribute that is added to this attribute.
get attributes():IAttributes { return this.p_value; }
get children():TNodes { return this.isCollection ? this.p_value : this.p_children; }
get valuePrevious():TValue { return <TValue>this.p_valuePrevious; }
protected p_valuePrevious:any;
get dataType():(DataType | string) { return this.p_dataType; }
protected p_dataType:(DataType | string);
/**
* Attributes will be considered empty if no key or value has been set.
*/
get isEmpty():boolean {
if (is.undefined(this.p_value)) return true;
if (is.empty(this.p_value, false)) return true;
if (is.empty(this.key, true)) return true;
if (is.empty(this.dataType, true)) return true;
if (this.dataType === DataType.UNDEFINED) return true;
if (this.dataType === DataType.NULL) return true;
return false;
}
/**
* If this function returns true, the attribute will be omitted from data transfer objects
* when saving data to the data store.
*/
get omitFromDataStore():(value:TValue) => boolean { return this.p_omitFromDataStore; }
set omitFromDataStore(fn:(value:TValue) => boolean) { this.p_omitFromDataStore = fn; }
protected p_omitFromDataStore:(value:TValue) => boolean;
get formatter():(value:any) => TValue { return this.p_formatter; }
set formatter(value:(value:any) => TValue) { this.p_formatter = value; }
protected p_formatter:(value:any) => TValue;
protected p_format(value:any):TValue { return this.p_formatter(value); }
get copy():TNode { return <TNode>this.create.fromObject(this.toObject()); }
/**
* An automatically generated unique ID that is used when animating attribute values.
*/
get animationKey():string {
if (is.empty(this._animationKey)) this._animationKey = this.key + '_' + Identifier.getSessionUniqueInteger();
return this._animationKey;
}
private _animationKey:string;
protected p_animation:IAnimationBatch;
get isEditable():boolean { return this.p_isEditable; }
set isEditable(value:boolean) {
this.p_isEditable = value;
}
protected p_isEditable:boolean;
get isHidden():boolean { return this.p_isHidden; }
set isHidden(value:boolean) {
this.p_isHidden = value;
}
protected p_isHidden:boolean;
get isAllowSave():boolean { return this.p_isAllowSave; }
set isAllowSave(value:boolean) {
if (this.isCollection) this.children.isAllowSave = value;
this.p_isAllowSave = value;
}
protected p_isAllowSave:boolean;
/**
* Should we allow accessor properties to be generated automatically when
* a new child Attribute is added to our Attribute collection?
*/
get isAllowAutoProperties():boolean { return this.p_isAllowAutoPropterties; }
set isAllowAutoProperties(value:boolean) { this.p_isAllowAutoPropterties = value; }
protected p_isAllowAutoPropterties:boolean;
/*====================================================================*
START: Signals
*====================================================================*/
get changed():Signal<AttributeEventArgs<TNode>> { return this.p_changed; }
protected p_changed:Signal<AttributeEventArgs<TNode>>;
/*====================================================================*
START: Internal
*====================================================================*/
/// Overridden to take isSave into account.
internalChanged(args:AttributeEventArgs<TNode>):void {
if (!this.isSignalChange && !this.isAllowSave) return;
if (
(args.changeOptions.isSignalChange || args.changeOptions.isSave) && this.isSignalChange
) this.changed.dispatch(args);
if (is.notEmpty(this.parentCollection) && (is.notEmpty(this.parentCollection.internalChanged))) {
this.parentCollection.internalChanged(args);
}
}
/*====================================================================*
START: Public Methods
*====================================================================*/
/**
* Save the value to the attribute.
*/
set(
value:any,
changeOptions:IAttributeChangeOptions = AttributeChangeOptions.default,
):IAttributeSetReturn {
let returnValue:IAttributeSetReturn = this.p_returnValue;
if (is.empty(value)) return returnValue;
if (this.create.getBaseDataType(this.p_dataType) === DataType.COLLECTION) {
/// This attribute holds a collection...
if (is.notEmpty(this.p_value) && (this.p_value instanceof AttributesBase)) {
let tracker:IAttributeUpdateTracker = this.p_value.update(value, changeOptions);
returnValue.updateTracker = tracker;
tracker.then((tracker:IAttributeUpdateTracker) => {
returnValue.isChanged = tracker.isChanged;
});
}
} else {
value = this.p_format(value);
if (this.p_value === value) {
if (changeOptions.action === AttributeChangeActions.SAVE) {
this.doChanged(changeOptions.action);
}
return returnValue;
}
this.p_valuePrevious = this.p_valueTarget;
this.p_value = value;
this.p_valueTarget = value;
returnValue.isChanged = true;
this.doChanged(changeOptions.action);
}
return returnValue;
}
/**
* Interrupt any animated value transitions that are in effect.
*/
interruptAnimation():void { if (is.notEmpty(this.p_animation)) this.p_animation.interrupt(); }
/**
* Manually trigger a dispatch on the 'changed' signal.
*/
doChanged(action:AttributeChangeActions):void {
if (!this.isSignalChange && !this.isAllowSave) return;
else if (action === AttributeChangeActions.SIGNAL_CHANGE && !this.isSignalChange) return;
else if (action === AttributeChangeActions.SAVE && !this.isAllowSave) return;
let args:AttributeEventArgs<TNode> = new AttributeEventArgs<TNode>(
<TNode><AttributeBase<any, any, any, any>>this,
NodeChangeType.UPDATED,
new AttributeChangeOptions(action).durationZero
);
this.internalChanged(args);
}
toObject(isIncludeDataTypes:boolean = true, isIncludeLabels:boolean = true, appendToObject?:any, isForceInclude:boolean = false):any {
if (!isForceInclude && (is.notEmpty(this.p_omitFromDataStore) && this.p_omitFromDataStore(this.p_value))) return appendToObject;
let object:any = {};
if (
isIncludeDataTypes ||
(isIncludeLabels && is.notEmpty(this.label, true) && this.label !== this.key)
) {
if (isIncludeDataTypes) object['dataType'] = this.dataType.toString();
if (isIncludeLabels && (is.notEmpty(this.label, true) && this.label !== this.key)) object['label'] = this.label;
object['isEditable'] = this.isEditable;
object['isHidden'] = this.isHidden;
if (this.p_value instanceof AttributesBase) {
object['value'] = (<IAttributes>this.p_value).toObject(isIncludeDataTypes, isIncludeLabels, isForceInclude);
} else object['value'] = this.formatted;
} else {
if (this.p_value instanceof AttributesBase) {
object = (<IAttributes>this.p_value).toObject(isIncludeDataTypes, isIncludeLabels, isForceInclude);
} else object = this.formatted;
}
if (is.notEmpty(appendToObject)) {
appendToObject[this.key] = object;
return appendToObject;
} else return object;
}
internalAttached(changeOptions:INodeChangeOptions):void {
if (is.notEmpty(this.parentCollection)) {
this.isSignalChange = this.parentCollection.isSignalChange;
this.isAllowSave = this.parentCollection.isAllowSave;
}
super.internalAttached(changeOptions);
}
} // End class
export default AttributeBase;