can-observable-object
Create observable objects used to manage state in explicitly defined ways.
class extends ObservableObject
Extending ObservableObject
creates a new class using the class body to configure props, methods, getters, and setters.
import { ObservableObject } from "can/everything";
class Scientist extends ObservableObject {
static props = {
name: String,
occupation: String
};
get title() {
return `${this.name}: ${this.occupation}`;
}
}
let ada = new Scientist({
name: "Ada Lovelace",
occupation: "Mathematician"
});
console.log( ada.title ); // -> "Ada Lovelace: Mathematician"
Use extends to create classes for models, ViewModels, and custom elements.
Returns
{Constructor}
:
An extended class that can be instantiated using new
.
new ObservableObject([props])
Calling new ObservableObject(props)
creates a new instance of ObservableObject or an extended ObservableObject. Then, new ObservableObject(props)
assigns every property on props
to the new instance. If props are passed that are not defined already, those properties are set on the instance. If the instance should be sealed, it is sealed.
import { ObservableObject } from "can/everything";
const person = new ObservableObject( {
first: "Justin",
last: "Meyer"
} );
console.log( person ); //-> {first: "Justin", last: "Meyer"}
Custom ObservableObject
types, with special properties and behaviors, can be defined with the extends signature.
Parameters
- props
{Object}
:Properties and values to seed the map with.
Mixed-in instance methods and properties
Instances of ObservableObject
have all methods and properties from
can-event-queue/map/map:
addEventListener - Register an event handler to be called when an event is dispatched.
@can.getWhatIChange - Return observables whose values are affected by attached event handlers
@can.isBound - Return if the observable is bound to.
@can.offKeyValue - Unregister an event handler to be called when an event is dispatched.
@can.onKeyValue - Register an event handler to be called when a key value changes.
dispatch - Dispatch event and key binding handlers.
listenTo - Listen to an event and register the binding for simplified unbinding.
off - A shorthand method for unbinding an event.
on - A shorthand method for listening to event.
one - Register an event handler that gets called only once.
removeEventListener - Unregister an event handler to be called when an event is dispatched.
stopListening - Stops listening for registered event handlers.
Example:
import { ObservableObject } from "can/everything";
class MyType extends ObservableObject {
static props = {
prop: String
};
}
const myInstance = new MyType( {prop: "VALUE"} );
myInstance.on( "prop", ( event, newVal, oldVal ) => {
console.log( newVal ); //-> "VALUE"
console.log( oldVal ); //-> "NEW VALUE"
} );
myInstance.prop = "NEW VALUE";
Observable class fields
ObservableObject
class fields are also observable:
import { ObservableObject } from "can/everything";
class MyType extends ObservableObject {
prop = "VALUE";
}
const myInstance = new MyType();
myInstance.on( "prop", ( event, newVal, oldVal ) => {
console.log( newVal ); //-> "VALUE"
console.log( oldVal ); //-> "NEW VALUE"
});
myInstance.prop = "NEW VALUE";
Mixed-in type methods and properties
Extended ObservableObject
classes have all methods and properties from
can-event-queue/type/type:
@can.offInstanceBoundChange - Stop listening to when an instance's bound status changes.
@can.offInstancePatches - Stop listening to patch changes on any instance.
@can.onInstanceBoundChange - Listen to when any instance is bound for the first time or all handlers are removed.
@can.onInstancePatches - Listen to patch changes on any instance.
Example:
import { ObservableObject, Reflect as canReflect } from "can/everything";
class MyType extends ObservableObject {
static props = {
prop: String
};
}
canReflect.onInstancePatches( MyType, ( instance, patches ) => {
console.log(patches) //-> {key:"prop", type:"set", value:"VALUE"}
} );
let instance = new MyType({prop: "value"});
instance.prop = "VALUE";
Overview
can-observable-object
is used to create easily extensible observable types with well defined behavior.
For example, a Todo
type, with a name
property, completed
property, and a toggle
method, might be defined like:
import { ObservableObject } from "can/everything";
class Todo extends ObservableObject {
static props = {
name: String,
completed: false // default value
};
toggle() {
this.completed = !this.completed;
}
}
const myTodo = new Todo({ name: "my first todo!" });
myTodo.toggle();
console.log( myTodo ); //-> {name: "my first todo!", completed: true}
The Object set on static define
defines the properties that will be
on instances of a Todo
. There are a lot of ways to define properties. The
DefinitionObject type lists them all. Here, we define:
name
as a property that will be type checked as aString
.completed
as a property that will be type check as aBoolean
with an initial value offalse
.
This also defines a toggle
method that will be available on instances of Todo
.
Todo
is a constructor function. This means instances of Todo
can be be created by
calling new Todo()
as follows:
import { ObservableObject } from "can/everything";
class Todo extends ObservableObject {
static props = {
name: String,
completed: false
};
toggle() {
this.completed = !this.completed;
}
}
const myTodo = new Todo();
myTodo.name = "Do the dishes";
console.log( myTodo.completed ); //-> false
myTodo.toggle();
console.log( myTodo.completed ); //-> true
Typed properties
ObservableObject uses can-type to define typing rules for properties. It supports both strict typing (type checking) and loose typing (type conversion).
If a property is specified as a specific type, ObservableObject will perform type checking. This means that if the first
property in the example below is set to any value that is not a string, an error will be thrown:
import { ObservableObject } from "can/everything";
class Person extends ObservableObject {
static props = {
first: String
};
}
const person = new Person();
person.first = "Justin"; // -> π
person.first = false; // -> Uncaught Error: Type value 'false' is not of type String.
can-type also supports functions like type.maybe and type.convert for handling other typing options. In the example below, maybeNumber
can be a number or null
or undefined
and alwaysString
will be converted to a string no matter what value is passed.
import { ObservableObject, type } from "can/everything";
class Obj extends ObservableObject {
static props = {
maybeNumber: type.maybe(Number),
alwaysString: type.convert(String)
};
}
const obj = new Obj();
obj.maybeNumber = 9; // -> π
obj.maybeNumber = null; // -> π
obj.maybeNumber = undefined; // -> π
obj.maybeNumber = "not a number"; // -> Uncaught Error: Type value 'not a number' is not of type Number.
obj.alwaysString = "Hello"; // -> π
obj.alwaysString = 9; // -> π, converted to "9"
obj.alwaysString = null; // -> π, converted to "null"
obj.alwaysString = undefined; // -> π, converted to "undefined"
To see all the ways types can be defined, check out the can-type docs.
Declarative properties
Arguably can-observable-object
's most important ability is its support of declarative properties
that functionally derive their value from other property values. This is done by
defining getter properties like fullName
as follows:
import { ObservableObject } from "can/everything";
class Person extends ObservableObject {
static props = {
first: String,
last: String
};
get fullName() {
return this.first + " " + this.last;
}
}
const person = new Person({
first: "Justin",
last: "Meyer"
});
console.log(person.fullName); //-> "Justin Meyer"
This property can be bound to like any other property:
import { ObservableObject } from "can/everything";
class Person extends ObservableObject {
static props = {
first: String,
last: String
};
get fullName() {
return this.first + " " + this.last;
}
}
const me = new Person({
first: "Harry",
last: "Potter"
});
me.on( "fullName", ( ev, newValue, oldValue ) => {
console.log( newValue ); //-> Harry Henderson
console.log( oldValue ); //-> Harry Potter
} );
me.last = "Henderson";
getter
properties use can-observation internally. This means that when bound,
the value of the getter
is cached and only updates when one of its source
observables change. For example:
import { ObservableObject } from "can/everything";
class Person extends ObservableObject {
static props = {
first: String,
last: String,
};
get fullName() {
console.log( "calculating fullName" );
return this.first + " " + this.last;
}
}
const hero = new Person( { first: "Wonder", last: "Woman" } );
hero.on( "fullName", () => {} );
console.log( hero.fullName ); // logs "calculating fullName", "Wonder Woman"
console.log( hero.fullName ); // "Wonder Woman"
hero.first = "Bionic"; // logs "calculating fullName"
hero.last = "Man"; // logs "calculating fullName"
console.log( hero.fullName ); // logs "Bionic Man"
Asynchronous properties
Properties can also be asynchronous using async(resolve)
. These are very useful when you have a type
that requires data from the server. For example, a ObservableObject might take a todoId
value, and want to make a todo
property available:
import { ObservableObject, ajax } from "can/everything";
class Todo extends ObservableObject {
static props = {
todoId: Number,
todo: {
async(resolve, lastSetValue) {
ajax( { url: "/todos/" + this.todoId } ).then( resolve );
}
}
};
}
Async props are passed a resolve
argument when bound. Typically in an application,
your template will automatically bind on the todo
property. But to use it in a test might
look like:
import { ObservableObject, ajax, fixture } from "can/everything";
class TodoViewModel extends ObservableObject {
static props = {
todoId: Number,
todo: {
async(resolve, lastSetValue) {
ajax( { url: "/todos/" + this.todoId } ).then( resolve );
}
}
};
}
fixture( "GET /todos/5", () => {
return { id: 5, name: "take out trash" };
} );
const todoVM = new TodoViewModel( { todoId: 5 } );
todoVM.on( "todo", function( ev, newVal ) {
console.log( newVal.name ) //-> "take out trash"
} );
console.log(todoVM.todo) //-> undefined
Getter limitations
There's some functionality that a getter or an asynchronous property can not describe declaratively. For these situations, you can use set or even better, use value.
For example, consider a state and city locator where you pick a United States state like Illinois and then a city like Chicago. In this example, we want to clear the choice of city whenever the state changes.
This can be implemented with set like:
import { ObservableObject, type } from "can/everything";
class Locator extends ObservableObject {
static props = {
state: {
type: String,
set() {
this.city = null;
}
},
city: type.maybe(String)
};
}
const locator = new Locator( {
state: "IL",
city: "Chicago"
} );
locator.state = "CA";
console.log( locator.city ); //-> null;
The problem with this code is that it relies on side effects to manage the behavior of
city
. If someone wants to understand how city
behaves, they might have to search all of the code for the Locator class.
The value behavior allows you to consolidate the
behavior of a property to a single place. For example, the following implements Locator
with value:
import { ObservableObject } from "can/everything";
class Locator extends ObservableObject {
static props = {
state: String,
city: {
value({ lastSet, listenTo, resolve }) {
// When city is set, update `city` with the set value.
listenTo( lastSet, resolve );
// When state is set, set `city` to null.
listenTo( "state", () => {
resolve( null );
} );
// Initialize the value to the `set` value.
resolve( lastSet.get() );
}
}
};
}
const locator = new Locator( {
state: "IL",
city: "Chicago",
} );
locator.state = "CA";
console.log( locator.city ); //-> null
While functional reactive programming (FRP) can take time to master at first, once you do, your code will be much easier to understand and
debug. The value behavior supports the basics of FRP programming - the ability to listen events and changes in other properties and resolve
the property to a new value.
Sealed instances and strict mode
By default, ObservableObject
instances are not sealed. This
means that setting properties that are not defined when the constructor is defined will be set on those instances anyway.
import { ObservableObject } from "can/everything";
class MyType extends ObservableObject {
static props = {
myProp: String
};
}
const myType = new MyType();
myType.otherProp = "value"; // no error thrown
Setting the extended ObservableObject to be sealed will instead result in throwing an error in files that are in strict mode. For example:
import { ObservableObject } from "can/everything";
class MyType extends ObservableObject {
static props = {
myProp: String
};
static seal = true;
}
const myType = new MyType();
try {
myType.otherProp = "value"; // error!
} catch(err) {
console.log(err.message);
}
Read the seal documentation for more information on this behavior.