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
andset
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
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.
- The name of the extension used in the
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
).
- The full property path (e.g.,
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).
- The previous value of the property (used during
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.
- A handler function for property
set: (newValue, e: PropertyAccessEvent) => any
- A handler function for property
set
operations.
- A handler function for property
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.
- A list of
setters: ((newValue, e: PropertyAccessEvent) => any)[]
- A list of
set
handler functions.
- A list of
changes: ((oldValue, newValue, e: PropertyAccessEvent) => void)[]
- A list of
change
handler functions.
- A list of
descriptor: PropertyDescriptor
- The property descriptor used in
Object.defineProperty
.
- The property descriptor used in
getError?: Error
- Stores any error that occurred during a
get
operation.
- Stores any error that occurred during a
setError?: Error
- Stores any error that occurred during a
set
operation.
- Stores any error that occurred during a
extension?: any
- An object for storing additional extension data.
get?: boolean
- Indicates if the property has a
get
handler.
- Indicates if the property has a
set?: boolean
- Indicates if the property has a
set
handler.
- Indicates if the property has a
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.
- If
alwaysFront?: boolean
- If
true
, the handler is always placed at the front, regardless of order.
- If
alwaysBack?: boolean
- If
true
, the handler is always placed at the back, regardless of order.
- If
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.
- Checks if the target object has the
implementOn(target: any, settings = PropertiesControllerSettings): boolean
- Implements the
PropertiesController
extension on the target object.
- Implements the
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'