UNPKG

@pilotlab/lux-attributes

Version:

A luxurious user experience framework, developed by your friends at Pilot.

292 lines (237 loc) 11.2 kB
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;