Concepts
Context

Models in Context

A key feature of Expressive is the ability to share state easily between components. This process is seamless and typesafe.

import Model, { Provider } from "@expressive/react";
 
class FooBar extends Model {
  foo = 1;
  bar = 2;
}
 
const Parent = () => (
  <Provider for={FooBar}>
    <Foo />
    <Bar />
  </Provider>
)

Included in @expressive/react is the Provider component, which takes an instance or model class as a prop. It then makes that model available to downstream components via a complementary Model.get method.

Note: Model.is here is a reference to the original state, used to update a model.

function Foo(){
  const { is: shared, foo } = FooBar.get();
 
  return (
    <div>
      Shared value foo is: {foo}!
      <button onClick={() => shared.bar++}>
        Click increments bar
      </button>
    </div>
  )
}
 
function Bar(){
  const { is: shared, bar } = FooBar.get();
 
  return (
    <div>
      Shared value bar is: {bar}!
      <button onClick={() => shared.foo++}>
        Click increments foo
      </button>
    </div>
  )
}

Foo: 0Click increments bar

Bar: 0Click increments foo

The Context Class

The Context class powers an ability to resolve and connect to controllers which are considered ambient for some other code. Mainly, it is exposed for use by adapters between MVC and another framework, such as @expressive/react. You will not normally use this class, but it can be helpful for building unit tests.

If shipping an adapter for MVC, it is recommended you do not re-export Context from @expressive/mvc, unlike Model. If your users really need this, they should import it directly.


How it works

A root Context is created using its constructor. Then on, it is built layer-by-layer, which each wrapping the same instance, via Object.create(this) internally.

Models and controllers (model instances) may be inputs to a given layer. If supplied a model, a controller is instantiated and used instead. As controllers are added, they are registered using their own Model types as keys.

However you resolve context, layers all share the same logic to fetch a controller from its associated Models. For a given context, the layer you have access to will contain all the controllers registered to that point.


Model Inheritance

When a controller is added, it is registered using its own and also inherited Model types. This means a controller can be found using any of it's subtypes. This enables, in a given context, code to seek a generic model and receive the nearest controller which satisfies it, however one which may have more info or features, specific to the situation.

class A extends Model {
  hello(){
    return 'hello world';
  }
}
 
class B extends A {
  name = "Bob";
 
  hello(){
    return `hello ${this.name}`;
  }
}
 
const context = new Context({ B });
 
context.get(A).hello(); // "hello Bob"

Here, the B controller is returned because it satisfies the A model. What's nice about this is that code calling for A doesn't need to know about the specifics of B in order to use it.


Implicit provision

Controllers, by default, are added automatically when direct children of one added to context. This makes composition easier, as separated concerns are accessible downstream, allowing for generic code to interact with a composed controller.

class B extends Model {
  hello = () => 'hello world';
}
 
class A extends Model {
  b = use(B);
}
 
const context = new Context({ A });
 
context.get(B).hello(); // "hello world"

Here, A will create a controller for B and is bound to it. If added to context, its child is also available to in context, so consumers can interact with B without needing to drill for it.


Conflicts

When inputs are added, they will often share one or Model type with one already added. This can cause issues because Models are the lookup keys. In this situation, the closest always wins on access.

However, a single layer can also have this problem. To account for this, the following rules apply when resolving one of multiple controllers defined by the same layer.

  • If there are multiple controllers, but only one is explicit, it is returned.
  • If multiple controllers are explicit, an error is thrown. You'll need a more specific key.
class Foo extends Model {}
class Bar extends Foo {}
 
const context = new Context({ Foo, Bar });
 
context.get(Bar) instanceof Bar; // true
context.get(Foo); // throws