Mutable
Mutable is an abstract class that allows to easily access its instance properties and watch changes on them. It may be used for model implementations.
To access the properties of an instance of Mutable directly, use dot notation or the functions get()
and set()
.
Usage
To define a Mutable instance, use the declare
function exported by "apprt-core/Mutable".
import {declare} from "apprt-core/Mutable";
const MyMutable = declare({
propA: "today",
propB: undefined
});
export default MyMutable;
The following sample shows how to watch changes on properties of a Mutable instance by using the watch
function:
import declare, { WatchEvent } from "apprt-core/Mutable";
function someFunction() {
const mutableInstance = new MyMutable();
mutableInstance.watch("propA", mutableCallback);
mutableInstance.propA = "tomorrow";
//equivalent to: mutableInstance.set("propA", "tomorrow");
}
function mutableCallback(evt: WatchEvent<"propA", string | undefined>) {
console.log(`Property name: ${evt.name}`); // "propA"
console.log(`Property current value: ${evt.value}`); // "tomorrow"
console.log(`Property old value: ${evt.old}`); // "today"
// NOTE: the old value is not always tracked due to performance reasons
}
To stop watching changes, call remove()
on the watch handle that is returned by a Mutable's watch
function:
const mutableInstance = new MyMutable();
const watchHandle = mutableInstance.watch("propA", (evt) => {
console.log(evt.value);
});
watchHandle.remove();
Extended Use Cases
Declare a property as not writable
You can prevent a property from being changed by setting writable: false
in its declaration (the default is true
):
import {declare} from "apprt-core/Mutable";
const MyMutable = declare({
defaultMessage: {
writable: false,
value : "default message"
}
});
const mutableInstance = new MyMutable();
mutableInstance.defaultMessage = "new message"; // does not change the value
console.log(mutableInstance.defaultMessage); // "default message"
Declare a property as required
To enforce that a property must be set during construction and with setter set required
to true
(default is false
):
import {declare} from "apprt-core/Mutable";
const MyClazz = declare({
msg : {
required: true
}
});
new MyClazz(); // throws an error because "msg" is not declared
const o = new MyClazz({msg:"a"}); // ok
o.msg = null; // throws an error
o.msg = undefined; // throws an error
o.msg = ""; // is ok because empty string is not forbidden
Declare a property as required using a default value
If the required: true
flag is used in combination with a default value, no error is thrown but the property is reset to the default value instead.
import {declare} from "apprt-core/Mutable";
const MyClazz = declare({
msg : {
value: "default",
required: true
}
});
const o = new MyClazz(); // ok
o.msg === "default"; // true
o.msg = undefined; // reset to o.msg === "default"
o.msg = ""; // o.msg === ""
Define the type of a property
To enforce that a property is of an exact type use the type
definition.
If a value is set to null
or undefined
, the type function is not called.
Use cast
in this cases.
import {declare} from "apprt-core/Mutable";
class MyType{
constructor(public x: string){ }
}
const MyClazz = declare({
msg : {
type: MyType
}
});
let o = new MyClazz();
o.msg === undefined; // type function not called
o = new MyClazz({
msg: { x: "a" }
});
console.log(o.msg instanceof MyType); // true
console.log(o?.msg?.x === "a"); // true
Use the cast function to intercept setting of values
To enforce custom logic during construction and setting of values you can define a custom cast
function.
Note you can only define a type
or a cast
function.
import {declare} from "apprt-core/Mutable";
const MyClazz = declare({
msg : {
cast: String
}
});
let o = new MyClazz({
msg: 1 as any
});
o.msg === "1"; // converted to string
o = new MyClazz({
msg: undefined
});
o.msg === "undefined"; // also undefined is converted to string
This is also a good way to introduce validation logic:
import { declare } from "apprt-core/Mutable";
const State = declare({
code: {
value: "ok",
cast(val: string) {
const allowed: Record<string, boolean> = {
ok: true,
wrong: true
};
if (!allowed[val]) {
throw new Error(`illegal code '${val}'`);
}
return val;
}
}
});
const o = new State();
o.code = "wrong"; // works
o.code = "ok"; // works
o.code = "what"; // throws error, because not "ok" and not "wrong"
Define getters and setters for custom behavior
If necessary you can define a custom getter and/or setter for a property. This lets you execute additional code if a value is changed or read.
To create a custom getter and setter for a property of a Mutable, add a set
and get
function as part of the property definition.
import {declare} from "apprt-core/Mutable";
const MyMutable = declare({
stuff: "default",
message: {
set(val: string) {
// do something with the value
this.stuff = val;
},
get: function(): string {
// calculate a return value";
return this.stuff! + "-suffix";
}
}
});
const myInstance = new MyMutable({
message: "default"
});
const propertyValue = myInstance.message; //"default-suffix"
Define only a setter to declare a write only property
To provide a property to set a state in a more complex object, you can provide a property that has a custom setter function.
import {declare} from "apprt-core/Mutable";
const MyMutable = declare({
coord : {
value: {
x : 0,
y : 0
}
},
x: {
set(val: number) {
this.coord!.x = val;
}
}
});
const myInstance = new MyMutable({});
myInstance.x = 10; // changes myInstances.coord.x;
Define only a getter to declare computed properties
If you like to provide a property to read a state from a more complex object or by reading other states, you can provide a property that only has a custom getter function. See also the depends declaration to ensure that computed properties gets updated when dependencies are changed.
import { declare } from "apprt-core/Mutable";
const MyMutable = declare({
coord: {
value: {
x: 0,
y: 0
}
},
x: {
get(): number {
return this.coord!.x;
}
}
});
const myInstance = new MyMutable({ coord: { x: 2, y: 2 } });
const x = myInstance.x; // shortcut for myInstances.coord.x;
Declare properties that are dependent on other properties using depends
If a property requires another property to be calculated correctly, this dependency needs to be declared. This forces the update of the property whenever one of the dependencies is changed.
import { declare } from "apprt-core/Mutable";
const Clazz = declare({
firstName: "",
lastName: "",
fullName: {
depends: ["firstName", "lastName"],
get(): string {
return `${this.firstName} ${this.lastName}`;
}
}
});
const o = new Clazz();
o.firstName = "Test"; // -> triggers watch event for "firstName" and "fullName".
Declare a custom constructor function using $construct
To add behavior during construction of a Mutable (for example to perform initial validation), use the $construct
key.
import { declare } from "apprt-core/Mutable";
const MyClazz = declare({
$construct(options = {}) {
// custom code, such as validation
// apply the super constructor (required)
this.$super(options);
},
// property declarations
msg: "ok"
});
Watch for any declared property change
mutableInstance.watch("*", evt => {
console.log("Property name: " + evt.name);
});
Declare watch logic with $watch on single property
To add logic to a Mutable which is triggered if a property is changed (for example to update other field values), use the following syntax:
import { declare } from "apprt-core/Mutable";
const Clazz = declare({
number: 1,
$watch: {
number(val: number) {
console.log(`number changed to ${val}`);
}
}
});
Declare watch logic with $watch on single private property
To add logic to a Mutable which is triggered if private properties are changed, use the following syntax:
import { declare } from "apprt-core/Mutable";
const _state = Symbol("_state");
const Clazz = declare({
[_state]: 1,
$watch: {
[_state](val: number) {
console.log(`number changed to ${val}`);
}
}
});
Declare watch logic with $watch on multiple properties
To add logic to a Mutable which is triggered if different properties are changed, use the following syntax:
import { declare } from "apprt-core/Mutable";
const _state = Symbol("_state");
const Clazz = declare({
number: 1,
[_state]: 1,
$watch: {
_react_on_state_and_number: {
depends: ["number", _state],
do() {
console.log("number or _state is changed");
}
}
}
});
This syntax can also be used to give a more informative name to the watch method.
Ensure that watch logic is triggered during construction
By default the $watch handlers are only triggered after an instance is constructed. To react on the initial properties, declare the flag triggerOnInit.
import { declare } from "apprt-core/Mutable";
const _state = Symbol("_state");
const Clazz = declare({
number: 1,
[_state]: 1,
$watch: {
_react_on_state_and_number: {
triggerOnInit: true,
depends: ["number", _state],
do() {
console.log("number or _state is changed");
}
}
}
});
Set and get a property of a nested object
The set
/get
method of Mutable support property path expressions like a.b.name
to access properties of sub objects.
Therefore, never use the .
char as part of a property name.
import { declare } from "apprt-core/Mutable";
const MyMutable = declare({
coord: {
x: 1,
y: 2
}
});
const a = new MyMutable();
a.get("coord.x"); // 1
a.set("coord.y", 4); // changes a.coord.y to 4
Use ES 2015 extends
syntax to define a Mutable
To create a class that extends the Mutable
class instead of using the declare
syntax as described previously, import the Mutable
class and use the properties
function from "apprt-core/Mutable" as in the following sample:
import { Mutable, properties } from "apprt-core/Mutable";
class MyMutable extends Mutable {}
properties(MyMutable, {
propA: {
value: "defaultValue"
}
});
Provide custom init function to provide programmatic default values
To ensure each Mutable instance get's its own property values, provide an init
function.
import { declare } from "apprt-core/Mutable";
const MyMutable = declare({
complexObj: {
init() {
return { x: { y: 5 } };
}
}
});
// init is called to provide any instance with own object instance,
// so that the object is not shared between instances.
const a = new MyMutable();
const b = new MyMutable();
a.complexObj !== b.complexObj; // true
Use Symbols to save private states in custom setters
To save private states in custom setters, use Symbols
as shown in the following sample:
import { declare } from "apprt-core/Mutable";
const _decryptedSecret = Symbol("decryptedSecret");
const Security = declare({
[_decryptedSecret]: "",
secret: {
set(encrypted: string) {
this[_decryptedSecret] = decrypt(encrypted);
},
get(): string {
return encrypt(this[_decryptedSecret]!);
}
}
});
const secure = new Security({
secret: "baba03432babab3r34"
});
Use Symbols to declare private properties
To manage private state as properties, use Symbols
.
import {declare} from "apprt-core/Mutable";
const internalMessage = Symbol("internal");
const MyMessage = declare({
[internalMessage] : "",
formatedMessage: {
get(): string{
const msg = this[internalMessage];
return msg ? `_${msg}_` : "";
}
}
});
const mymsg = new MyMessage({});
// only clients which have access to the symbol instance can write the property
mymsg[internalMessage] = "new message";
Caveats
Watching undeclared properties
It is not possible to watch for dynamic added properties, because Mutable implements the "Accessor" pattern - that means the following does not work:
import { declare } from "apprt-core/Mutable";
const MyMutable = declare({
msg: "a"
});
const a = new MyMutable();
a.watch("x" as any, (v) => {
console.log(v);
});
// dynamic add "x" to the mutable, this cannot be detected by Mutable and does not throw any event.
(a as any).x = "hello";
Watching computed properties
Watch events for computed properties (only getter) are only thrown if the depends
declaration is correct.
If you do not declare depends
, the Mutable code cannot track that the computed property must be recalculated.
This sample shows a computed property fullName
which also triggers "watch" events:
import { declare } from "apprt-core/Mutable";
const Clazz = declare({
firstName: "",
lastName: "",
fullName: {
// trigger watch if these properties are changed
depends: ["firstName", "lastName"],
get(): string {
return `${this.firstName} ${this.lastName}`;
}
}
});
const o = new Clazz();
o.watch("fullName", (evt) => console.log(evt.value));
o.firstName = "Test"; // triggers watch "firstName" and "fullName"
Manual trigger watch of a computed property
Computed properties may change based custom events.
To inform about that external change the method notifyChange
is provided.
This sample shows how the method is used:
import { declare, notifyChange } from "apprt-core/Mutable";
let externalState = 0;
const Clazz = declare({
state: {
get() {
return externalState;
}
}
});
const o = new Clazz();
o.watch("state", (evt) => console.log(evt.value));
// change the state
externalState++;
// inform about the change
notifyChange(o, "state");
Be careful when using complex default values
Complex default values like arrays and objects and class instances are shared between all Mutable instances, which may lead to unwanted side effects. Please consider this sample:
import {declare} from "apprt-core/Mutable";
const MyMutable = declare({
// no problem immutable value
string : "a",
// no problem immutable value
number: 2,
// no problem array with immutable values is cloned
array1: ["a", "b"],
// problem array is cloned, but not deep the object is shared between instances
array2: [{x:1}],
// no problem x is immutable value and object is cloned using Object.assign
obj1 : { x: 1 },
// problem, value of x is shared between instances
obj2 : { x : { y : 1}}
});
const a = new MyMutable();
const b = new MyMutable();
a.array1![0] = "c"; // ok b is not changed
a.array2![0]!.x = 2; // changes b instance
a.obj1.x = 2; // ok b not changed
a.obj2.x.y = 2; // changes b instance
To overcome this problem declare a clone
method, which is responsible to create the default values.