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.