Skip to main content

Properties Controller

PropertiesController module is the base module used by the dataclass models dealing with property get() and set() operations. Many extensions require PropertiesController to make sure pre/post callbacks are called upon property access of the model data.

The Properties Controller module is designed to provide fine-grained control over property access in TypeScript classes. By utilizing this module, developers can:

  • Intercept property get and set operations.
  • Apply custom validation, transformation, or side effects during property access.
  • Manage property access events and handle errors or cancellations gracefully.
  • Extend classes with property control capabilities without modifying their original implementation.

Table of Contents

  1. Getters/Setters
  2. Classes and Interfaces
  3. Usage Examples
  4. Conclusion

PropertiesController - Getters/Setters

Most extensions are built on the base class PropertiesController which does a lot more general handling of getters, setters, and onvaluechage events.

import { defineOn } from 'ts-basis';
class MyClass {
myProp1 = 'firstValue';
myProp2 = 300;
constructor(init?: Partial<MyClass>) {
defineOn(this, MyClass, lib => {
const manageOptions = {}; // options like 'prepend', 'alwaysFront', 'alwaysBack', 'order'
lib.propertiesController.manage(this, manageOptions, {
myProp1: {
set(value, e) { console.log(`setter: ${e.path} being set '${value}'`); },
get(value, e) { console.log(`getter: ${e.path} being accessed`); },
change(oldValue, newValue, e) { console.log(`onchange: ${e.path} changed from '${oldValue}' to '${newValue}'`); },
}
});
// manage function is additive in terms of handlers
lib.propertiesController.manage(this, manageOptions, {
myProp1: {
set(value, e) { console.log(`setter 2`); },
get(value, e) { console.log(`getter 2`); },
change(oldValue, newValue, e) { console.log(`onchange 2`); },
}
});
}
}

const a = new MyClass();
a.myProp1 = a.myProp1 + '2';
// getter: MyClass.myProp1 being accessed
// getter 2
// setter: MyClass.myProp1 being set 'firstValue2'
// setter 2
// onchange: MyClass.myProp1 changed from 'firstValue' to 'firstValue2'
// onchange 2

Classes and Interfaces

Below is a detailed breakdown of the classes and interfaces provided by the Properties Controller module.


PropertiesControllerSettings

Extends: TypeToolsSettings

The PropertiesControllerSettings class holds global and instance settings for the PropertiesController. It allows for configuring the behavior of property control on a global or per-instance basis.

export class PropertiesControllerSettings extends TypeToolsSettings {
static disabledGlobally = false;
static extensionPropertiesController = 'PropertiesController';
extensionPropertiesController = PropertiesControllerSettings.extensionPropertiesController;

constructor(init?: Partial<PropertiesControllerSettings>) {
super(init);
if (init) {
Object.assign(this, init);
}
}
}

Properties

  • disabledGlobally: boolean (static)
    • Determines if property control is disabled globally.
  • extensionPropertiesController: string
    • The name of the extension used in the TypeTools infrastructure.

PropertyAccessTrace

The PropertyAccessTrace class is used to store traces of property access events, particularly for error handling and debugging purposes.

export class PropertyAccessTrace {
trace: Error;
e: PropertyAccessEvent;
}

Properties

  • trace: Error
    • The error trace associated with the property access.
  • e: PropertyAccessEvent
    • The event data related to the property access.

PropertyAccessEvent

The PropertyAccessEvent class encapsulates the context and data related to a property access event. It provides methods to control property access flow, such as cancelling the operation, throwing errors, or transforming the property value.

export class PropertyAccessEvent {
property: string;
className: string;
classRealPath: string;
path: string;
class: any;
data: { [flagName: string]: any } = {};
ignoredClasses: Class<any>[] = [];
value: any;
oldValue: any;
thrown: boolean;

constructor(init?: Partial<PropertyAccessEvent>) {
if (init) {
Object.assign(this, init);
}
}

// Methods...
}

Properties

  • property: string
    • The name of the property being accessed.
  • className: string
    • The name of the class where the property resides.
  • classRealPath: string
    • The full path of the class.
  • path: string
    • The full property path (e.g., ClassName.property).
  • class: any
    • The class object.
  • data: { [flagName: string]: any }
    • A data map for storing custom flags or information during the event.
  • ignoredClasses: Class<any>[]
    • A list of classes to ignore for definitions during the event handling.
  • value: any
    • The current value of the property.
  • oldValue: any
    • The previous value of the property (used during set operations).
  • thrown: boolean
    • Indicates if an error was thrown during the event.

Methods

  • clearData()
    • Resets the event data.
  • cancel(message?: string)
    • Cancels the property access operation with an optional message.
  • throw(message?: string)
    • Throws an error to stop the property access operation.
  • stopPropagation()
    • Stops further propagation of the event to other handlers.
  • transformValue(newValue: any)
    • Transforms the property value during the event handling.
  • getStackTrace()
    • Retrieves the stack trace for debugging purposes.
  • ignoreDefinitionsFrom<T = any>(...classes: Class<T>[])
    • Specifies classes to ignore when handling the event.

PropertyControlLayer

The PropertyControlLayer class defines the control logic for property access. It allows you to define custom get, set, and change handlers for properties.

export class PropertyControlLayer {
get: (value, e: PropertyAccessEvent) => any;
set: (newValue, e: PropertyAccessEvent) => any;
change: (oldValue, newValue, e: PropertyAccessEvent) => void;
throwError: boolean = true;

constructor(init?: Partial<PropertyControlLayer>) {
if (init) {
Object.assign(this, init);
}
}
}

Properties

  • get: (value, e: PropertyAccessEvent) => any
    • A handler function for property get operations.
  • set: (newValue, e: PropertyAccessEvent) => any
    • A handler function for property set operations.
  • change: (oldValue, newValue, e: PropertyAccessEvent) => void
    • A handler function for when a property's value changes.
  • throwError: boolean
    • Determines if an error should be thrown when an exception occurs.

PropertyController

The PropertyController class manages the control layers for individual properties, keeping track of the value and handling the get, set, and change operations.

export class PropertyController {
property: string;
valueKeeper: { value: any };
getters: ((value, e: PropertyAccessEvent) => any)[];
setters: ((newValue, e: PropertyAccessEvent) => any)[];
changes: ((oldValue, newValue, e: PropertyAccessEvent) => void)[];
descriptor: PropertyDescriptor;
getError?: Error;
setError?: Error;
extension?: any;
get?: boolean;
set?: boolean;

constructor(init?: Partial<PropertyController>) {
if (init) {
Object.assign(this, init);
}
}

// Methods...
}

Properties

  • property: string
    • The name of the property being controlled.
  • valueKeeper: { value: any }
    • An object that holds the current value of the property.
  • getters: ((value, e: PropertyAccessEvent) => any)[]
    • A list of get handler functions.
  • setters: ((newValue, e: PropertyAccessEvent) => any)[]
    • A list of set handler functions.
  • changes: ((oldValue, newValue, e: PropertyAccessEvent) => void)[]
    • A list of change handler functions.
  • descriptor: PropertyDescriptor
    • The property descriptor used in Object.defineProperty.
  • getError?: Error
    • Stores any error that occurred during a get operation.
  • setError?: Error
    • Stores any error that occurred during a set operation.
  • extension?: any
    • An object for storing additional extension data.
  • get?: boolean
    • Indicates if the property has a get handler.
  • set?: boolean
    • Indicates if the property has a set handler.

Methods

  • orderHandlers()
    • Orders the handlers based on specified order rules.
  • orderHandler(handlers: any[])
    • Helper function to order individual handlers.

Note: The orderHandlers and orderHandler methods are used internally to sort the handlers, ensuring that any handlers marked to always be at the front or back are positioned correctly.


PropertiesManagementOptions

An interface that defines options for managing property control layers.

export interface PropertiesManagementOptions {
prepend?: boolean;
alwaysFront?: boolean;
alwaysBack?: boolean;
order?: number;
}

Properties

  • prepend?: boolean
    • If true, new handlers are added to the front of the handlers list.
  • alwaysFront?: boolean
    • If true, the handler is always placed at the front, regardless of order.
  • alwaysBack?: boolean
    • If true, the handler is always placed at the back, regardless of order.
  • order?: number
    • Numerical order to position the handler within the list.

PropertiesControllerExtensionData

Implements TypeToolsExtensionData

This class holds extension data specific to the PropertiesController. It stores managed properties, error and cancel traces, and event handlers.

export class PropertiesControllerExtensionData implements TypeToolsExtensionData {
managed: { [propName: string]: PropertyController };
errors?: PropertyAccessTrace[];
cancels?: PropertyAccessTrace[];
onerrors: ((tracer: PropertyAccessTrace) => any)[];
oncancels: ((tracer: PropertyAccessTrace) => any)[];
onpropertychanges?: ((
propName: string,
oldValue: any,
newValue: any,
immediate?: boolean,
) => any)[];
}

Properties

  • managed: { [propName: string]: PropertyController }
    • A map of property names to their controllers.
  • errors?: PropertyAccessTrace[]
    • A list of error traces occurred during property operations.
  • cancels?: PropertyAccessTrace[]
    • A list of cancel traces occurred during property operations.
  • onerrors: ((tracer: PropertyAccessTrace) => any)[]
    • Event handlers for errors.
  • oncancels: ((tracer: PropertyAccessTrace) => any)[]
    • Event handlers for cancellations.
  • onpropertychanges?: ((propName: string, oldValue: any, newValue: any, immediate?: boolean) => any)[]
    • Event handlers for property value changes.

PropertiesController

Implements: TypeToolsExtension

The PropertiesController class is the main extension that applies property control to target objects. It provides static methods for managing property access and can be used to implement property control on any object.

export class PropertiesController implements TypeToolsExtension {
settings: PropertiesControllerSettings;

constructor(settings?: Partial<PropertiesControllerSettings>) {
this.settings = settingsInitialize(PropertiesControllerSettings, settings);
}

// Static Methods...
// Instance Methods...
}

Static Methods

  • getExtensionData(target: any, settings = PropertiesControllerSettings): PropertiesControllerExtensionData
    • Retrieves the extension data associated with the target object.
  • typeCheck(target: any, settings = PropertiesControllerSettings): boolean
    • Checks if the target object has the PropertiesController extension applied.
  • implementOn(target: any, settings = PropertiesControllerSettings): boolean
    • Implements the PropertiesController extension on the target object.
  • manage<T = any>(target: T, options: PropertiesManagementOptions, rubric: PartialCustom<T, Partial<PropertyControlLayer>>, settings = PropertiesControllerSettings)
    • Manages property control layers for the target object based on the provided rubric.
  • getErrorTracesOf(target: any, settings = PropertiesControllerSettings): PropertyAccessTrace[]
    • Retrieves the error traces for the target object.
  • getCancelTracesOf(target: any, settings = PropertiesControllerSettings): PropertyAccessTrace[]
    • Retrieves the cancel traces for the target object.

Instance Methods

  • getExtensionData(target: any): PropertiesControllerExtensionData
    • Instance version of the static method.
  • typeCheck(target: any): boolean
    • Instance version of the static method.
  • implementOn(target: any): boolean
    • Instance version of the static method.
  • manage<T = any>(target: T, options: PropertiesManagementOptions, rubric: PartialCustom<T, Partial<PropertyControlLayer>>)
    • Instance version of the static method.

Usage

The PropertiesController can be used to add property control to any object. Here's how you might use it:

// Create a target object
const targetObject = { name: 'Alice', age: 30 };

// Create an instance of PropertiesController
const propertiesController = new PropertiesController();

// Implement property control on the target object
propertiesController.implementOn(targetObject);

// Define property control layers
propertiesController.manage(targetObject, {}, {
name: {
get(value, e) {
console.log(`Getting name: ${value}`);
return value;
},
set(newValue, e) {
if (typeof newValue !== 'string') {
e.throw('Name must be a string');
return false;
}
console.log(`Setting name to: ${newValue}`);
},
},
age: {
set(newValue, e) {
if (newValue < 0) {
e.cancel('Age cannot be negative');
return false;
}
},
},
});

// Access the properties
console.log(targetObject.name); // Getting name: Alice
targetObject.name = 'Bob'; // Setting name to: Bob
targetObject.age = -5; // Operation cancelled: Age cannot be negative

Usage Examples

Example 1: Validating Property Values

const user = { username: 'john_doe', email: 'john@example.com' };
const controller = new PropertiesController();

// Implement property control on the user object
controller.implementOn(user);

// Manage properties with validation
controller.manage(user, {}, {
email: {
set(newValue, e) {
const emailRegex = /\S+@\S+\.\S+/;
if (!emailRegex.test(newValue)) {
e.throw('Invalid email format');
return false;
}
},
},
});

// Attempt to set an invalid email
try {
user.email = 'invalid_email'; // Throws error: Invalid email format
} catch (error) {
console.error(error.message);
}

Example 2: Transforming Property Values

const product = { price: 100 };
const controller = new PropertiesController();

controller.implementOn(product);

controller.manage(product, {}, {
price: {
set(newValue, e) {
// Ensure the price is always stored as a number
const numericValue = parseFloat(newValue);
if (isNaN(numericValue)) {
e.cancel('Price must be a number');
return false;
}
e.transformValue(numericValue.toFixed(2)); // Transform to two decimal places
},
},
});

product.price = '150.5678'; // price is now '150.57'
console.log(product.price); // Outputs: '150.57'