Core
Model

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)
  • Controller (instance)
    • is - Reference to original this, 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.


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.


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.