Model
Models are a type of class, which defines state for a given application. They are nearly empty, opinionated and can be used for any purpose. The intent is to extend them to create custom behavior.
Instances of a model are called controllers, they are responsible for the state and logic expressed by one or more components which use them.
The base Model does have a small number of properties and methods however.
They are designed to be inherited and used by with your own custom models.
For example, if class Foo extends Model {}
, then Foo.new()
would
create a new controller of type Foo
to be used.
Note: This page includes what a Model
looks like when imported from @expressive/mvc
.
Flavors of MVC (such as @expressive/react
) will augment Model
with additional methods.
- Model (class)
constructor(...)
- Base constructor for all models.new(...)
- Create controller, instance of this model.is(model)
- Check if argument extends this model.on(callback)
- Callback on any event from type of controller.
- Controller (instance)
is
- Reference to originalthis
, useful from inside a subscriber.get(...)
- Export or otherwise observe state.set(...)
- Dispatch or respond to events and/or updates.toString()
- Obtain unique (or given) ID of a controller.Symbol.iterator
- Scan managed properties using for loops.
Model (class)
Constructor
constructor(id?: string | number)
You should always call Model.new()
to instantiate a model.
Using new Model(...)
will not automatically run a necessary bootstrap step resulting in
a controller not being ready or stateful.
When creating a Model, it will be given an id
.
If not specified, a random one is generated. (e.g. FooBar-A1234
)
Otherwise, a string or number can be supplied.
Uniqueness in user-defined IDs is not enforced, but highly recommended.
Model.new
Model.new (...args: ConstructorParameters<this>): InstanceType<this>
This method is used to create and activate a new instance of any given model.
It is recommended over new Model()
as it will bootstrap state before returning,
also having run instructions defined by the model.
Model.is
Model.is <T extends Model.Type> (this: T, maybe: any): maybe is T
This static method is used to check if a given model is an extension of another.
Consider you have a custom Model which extends another.
class Foo extends Model {
static method(){}
public method(){}
}
class Bar extends Foo {
/* ... */
}
With the static is
method, one can check if Bar
does extend Foo
.
This is particularly helpful in typescript, to narrow the type of a model which is unknown.
// Assume we only know this is a model, but not what kind.
const Type = Bar as Model.New;
// @ts-expect-error: Property 'method' does not exist on type 'New<Model>'.
Type.method();
const type = Type.new();
// @ts-expect-error: Property 'method' does not exist on type 'Model'.
type.method();
if(Foo.is(Type)){
// Typescript now knows `Type` is at-least a Foo constructor.
// Static and instance methods defined by it become available as a result.
Type.method();
const type = Type.new();
// also valid because `type` is known to be instanceof Foo.
type.method();
}
Model.on
Model.on <T extends Model> (this: Model.Type<T>, event: Model.Event<T>): () => boolean
This method will register a callback to be run when any event is dispatched by a type of model.
First argument provided (event
) is an event listener. Method returns a function which can be used to unsubscribe.
class Foo extends Model {
value = 0;
}
Foo.on((key, source) => {
console.log(`${source} dispatched '${key}'.`);
})
const foo = Foo.new();
// Foo-A1234 dispatched 'true'.
foo.set("event")
// Foo-A1234 dispatched 'event'.
foo.value = 1;
// Foo-A1234 dispatched 'value'.
await foo.set();
// This waits for update to settle.
// Foo-A1234 dispatched 'false'.
foo.set(null);
// Foo-A1234 dispatched 'null'.
Callbacks applied to a model will also be run for any model which extends it.
To respond to events from any controller, for instance, you might use Model.on()
.
Model.on((key, source) => {
if(key === true)
console.log(`${source} was just created!`);
})
Foo.new();
// Foo-A1234 was just created!
Controller (instance)
model.is
The is
property is a reference to unwrapped this
for a controller.
This is useful when working within a subscriber which receives a sort-of proxy.
Because destructuring is the recommended way to access state, this provides access
to the controller for assigning new values.
class Control extends Model {
value = 0;
}
function Component(){
const { is: control, value } = Control.use();
return (
<button onClick={() => control.value++}>
{value}
</button>
);
}
Control.use()
is defined only for projects using @expressive/react
.
model.get
The get method is an overloaded method which can be used to fetch or observe properties. Check Model.get, or one of the following overloads, for more documentation.
-
Get values from state
get(): State<this>
-
Create a subscriber
get(effect: Effect<this>): () => void
-
Get value from state
get<T>(key: T, required?: boolean): Value<this, T>
-
Call a function when update occurs
get<T>(key: T, callback: OnUpdate<this, T>): () => void
-
Check if model is expired
get(status: null): boolean
-
Callback on destroyed
get(status: null, callback: () => void): () => void
model.set
The set method is an overloaded method which can be used to dispatch or respond to events. Check Model.set, or one of the following overloads, for more documentation.
-
Get update in progress
set(): Promise<Values<this>> | undefined
-
Call a function when event occurs
set(callback: Event<this>): () => boolean
-
Dispatch an event
set(key: Key<this>): void
-
Update a property
set<K>(key: K, value?: Value<this, K>, silent?: boolean): void
-
Destroy a controller
set(status: null): void
model.toString
The toString
method is used to obtain a unique ID for a controller.
It can be accessed the following ways:
Template:
const foo = Foo.new();
console.log(`Hello ${foo}`);
// Hello Foo-A1234
Coercion:
const id = String(Foo.new());
console.log(id);
// Foo-A1234
Explicit:
const id = Foo.new().toString();
console.log(id);
// Foo-A1234
model[Symbol.iterator]
Model instances are iterable, meaning they can be used with for-of loops.
This behavior work similarly to a Map
.
class Control extends Model {
foo = 1;
bar = 2;
baz = 3;
}
const control = Control.new();
for(const [key, value] of control)
console.log(key, value);
// foo 1
// bar 2
// baz 3
This is particularly useful for instructions, which may need to know what properties exist on their parent controller.