@pilotlab/lux-attributes
Version:
A luxurious user experience framework, developed by your friends at Pilot.
292 lines (237 loc) • 11.2 kB
text/typescript
import is from '@pilotlab/lux-is';
import { MapList } from '@pilotlab/lux-collections';
import { NodesBase } from '@pilotlab/lux-nodes';
import { ProgressTracker } from '@pilotlab/lux-progress';
import { IPromise, Result } from '@pilotlab/lux-result';
import { Signal } from '@pilotlab/lux-signals';
import IAttribute from './interfaces/iAttribute';
import AttributeCreateOptions from './attributeCreateOptions';
import AttributeSetReturn from './attributeSetReturn';
import { DataType } from './attributeEnums';
import AttributeChangeOptions from './attributeChangeOptions';
import IAttributeChangeOptions from './interfaces/iAttributeChangeOptions';
import IAttributeCreateOptions from './interfaces/iAttributeCreateOptions';
import IAttributes from './interfaces/iAttributes';
import IAttributeUpdateTracker from './interfaces/iAttributeUpdateTracker';
import AttributeUpdateTracker from './attributeUpdateTracker';
import IAttributeFactory from './interfaces/iAttributeFactory';
import AttributeEventArgs from './attributeEventArgs';
import IAttributeSetReturn from './interfaces/iAttributeSetReturn';
export abstract class AttributesBase<
TNode extends IAttribute,
TNodes extends IAttributes
>
extends NodesBase<TNode>
implements IAttributes {
constructor(
factory:IAttributeFactory,
parent?:TNode,
pathSegment:string = '.'
) {
super(factory, parent, pathSegment);
this.p_factory = factory;
}
/*====================================================================*
START: Factory
*====================================================================*/
get create():IAttributeFactory { return this.p_factory; }
protected p_factory:IAttributeFactory;
/*====================================================================*
START: Properties
*====================================================================*/
get copy():TNodes {
return <TNodes>this.create.collection.fromObject(this.toObject());
}
/*====================================================================*
START: Signals
*====================================================================*/
saveTriggered:Signal<AttributeEventArgs<any>> = new Signal<AttributeEventArgs<any>>();
/*====================================================================*
START: Methods
*====================================================================*/
/**
* If an attribute exists at the given path, it will be returned.
* Otherwise, if createOptions is supplied, a new attribute will
* first be created with the given parameters, then returned.
*/
getOrCreate(
path:string,
createOptions?:IAttributeCreateOptions,
segmentCreateOptions?:IAttributeCreateOptions
):TNode {
if (is.empty(createOptions)) createOptions = new AttributeCreateOptions(null, DataType.COLLECTION);
if (is.empty(segmentCreateOptions)) segmentCreateOptions = new AttributeCreateOptions(null, DataType.COLLECTION);
return super.getOrCreate(path, createOptions, segmentCreateOptions);
}
get(
path:string,
value?:any,
dataType?:DataType,
label?:string,
changeOptions:IAttributeChangeOptions = AttributeChangeOptions.default
):TNode {
const createOptions:IAttributeCreateOptions = new AttributeCreateOptions(value, dataType, label, changeOptions);
const segmentCreateOptions:IAttributeCreateOptions = new AttributeCreateOptions(null, DataType.COLLECTION, null, changeOptions);
return this.getOrCreate(path, createOptions, segmentCreateOptions);
}
getOnly(paths:string[]):TNodes {
let collection:TNodes = <TNodes>this.create.collection.instance();
paths.forEach((path:string) => {
let child:TNode = this.get(path);
/// Return the child attributes by reference here, since that's what get() does.
/// If the user doesn't want to modify the actual attributes, they can call .copy on the returned collection.
if (is.notEmpty(child) && !child.isEmpty) collection.add(child, null, AttributeChangeOptions.zero);
});
return collection;
}
getAll(key?:string, options?:any, filter?:(data:any) => boolean, sort?:(a:any, b:any) => number):TNodes {
let collectionIn:TNodes;
let collectionOut:TNodes = <TNodes>this.create.collection.instance();
/// Retrieve only child attribute collections with a child key that matches the key argument.
if (is.notEmpty(key)) {
collectionIn = <TNodes>this.create.collection.instance();
this.forEach((childLevel1:TNode) => {
if (childLevel1.dataType === DataType.COLLECTION) {
childLevel1.value.forEach((nodeLevel2:TNode) => {
if (nodeLevel2.key === key) {
let isInclude:boolean = true;
/// Were any options passed in?
if (is.notEmpty(options)) {
/// If a key was passed in as an option, check to see whether the value of the attribute matches the key.
if (is.notEmpty(options.key)) if (nodeLevel2.value !== options.key) isInclude = false;
}
if (isInclude) collectionIn.set(<string>childLevel1.key, childLevel1.value);
}
});
return true;
}
return true;
});
} else collectionIn = <TNodes><IAttributes>this;
if (is.notEmpty(filter)) {
collectionIn.list.forEach((child:TNode):boolean => {
if (filter(child.toObject())) collectionOut.set(<string>child.key, child.value);
return true;
});
} else collectionOut = collectionIn;
if (is.notEmpty(sort)) collectionOut.sort(sort);
else {
collectionOut.list.sort((a:TNode, b:TNode) => {
if (a.value.get(key).value > b.value.get(key).value) return 1;
else if (a.value.get(key).value < b.value.get(key).value) return -1;
return 0;
});
}
return <TNodes>collectionOut;
}
/**
* If an attribute doesn't already exist at the given path, one will be created.
*/
set(
path:string,
value:any,
changeOptions?:IAttributeChangeOptions
):IPromise<IAttributeSetReturn> {
let result:IPromise<IAttributeSetReturn> = new Result<IAttributeSetReturn>();
if (is.empty(path)) return result.resolve(new AttributeSetReturn<TNode>(null, false));
const createOptions:IAttributeCreateOptions = new AttributeCreateOptions(value, null, null, changeOptions);
const segmentCreateOptions:IAttributeCreateOptions = new AttributeCreateOptions(null, DataType.COLLECTION, null, changeOptions);
let node:TNode = <TNode>super.getOrCreate(path, createOptions, segmentCreateOptions);
if (is.notEmpty(node)) {
node.set(
value,
changeOptions
).then((returnValue:AttributeSetReturn<TNode>) => {
result.resolve(returnValue);
});
}
return result;
}
deleteByKey(
key:string,
changeOptions:IAttributeChangeOptions = AttributeChangeOptions.default
):TNode { return this.deleteByKey(key, changeOptions); }
interruptAll():void {
this.p_list.forEach(function (child:TNode):boolean {
if (is.empty(child)) return true;
child.interrupt();
return true;
});
}
toMap(isIncludeDataTypes:boolean = false):MapList<string, any> {
let map:MapList<string, any> = new MapList<string, any>();
this.p_list.forEach(function (child:TNode):boolean {
if (is.empty(child)) return true;
map.set(<string>child.key, child.toObject(isIncludeDataTypes));
return true;
});
return map;
}
toObject(isIncludeDataTypes:boolean = false, isForceIncludeAll:boolean = false):Object {
let object:Object = {};
this.p_list.forEach((node:TNode):boolean => {
if (is.empty(node)) return true;
node.toObject(isIncludeDataTypes, object, isForceIncludeAll);
return true;
});
return object;
}
toArray():any[] {
let array:any[] = [];
for (let i:number = 0; i < this.p_list.size; i++) {
if (is.notEmpty(this.p_list.item(i))) array[i] = this.p_list.item(i).toObject(false);
}
return array;
}
toJson(isIncludeDataTypes:boolean = false):string {
return JSON.stringify(this.toObject(isIncludeDataTypes));
}
/*====================================================================*
START: Update
*====================================================================*/
/**
* <pre><code>
* /// Just quietly initialize the data entity, but don't save to the data store or signal a change.
* /// This is used when reloading data from the store to initialize the app.
*
* myComponent.data.update(data, AttributeChangeOptions.noSaveOrSignal);
*
* /// Update the data, and signal changes, but don't save to the data store.
* /// Used for data entities that don't need to be saved to a store.
*
* myComponent.data.update(data, nodeChangeOptions.flags(AttributeChangeOptions.SIGNAL_CHANGE));
*
* /// Update the data and save to the data store, but don’t signal a change.
* /// Used for batch saving, when you may want to signal changes only at the end of the process.
*
* myComponent.data.update(data, nodeChangeOptions.flags(AttributeChangeOptions.SAVE));
*
* /// And, finally, the default mode. Update the data, save to the store and signal the change.
*
* myComponent.data.update(data, AttributeChangeOptions.saveAndSignal);
* </code></pre>
*/
update(
data:(IAttributes | Object | string),
changeOptions:IAttributeChangeOptions = AttributeChangeOptions.default,
progressTracker?:ProgressTracker
):IAttributeUpdateTracker {
let attributesNew:IAttributes = this.create.collection.fromAny(data);
let updateTracker:IAttributeUpdateTracker = new AttributeUpdateTracker(attributesNew, changeOptions, progressTracker);
if (is.empty(attributesNew) || attributesNew.size === 0) {
updateTracker.progressTracker.run();
return updateTracker;
}
this.updateTracked(updateTracker);
return updateTracker;
}
/**
* Cascades data down through all attributes and attribute
* collections to update any that need to be changed.
*/
updateTracked(updateTracker:IAttributeUpdateTracker):void {
updateTracker.update(this);
updateTracker.progressTracker.run();
}
} // End class
export default AttributesBase;