Model is the basic unit to encapsulate State, together with some functions to manipulate states. A Model:
- defines the shape/type of the data (
propTypes
) and comes with default values (default
) - defines the shape/type of context data that it provides (
childContextTypes
andgetChildContext()
) or depends on (contextTypes
) - defines bubbling events that it fires (
eventTypes
) or watches (watchEventTypes
andwatchEvent()
) - contains pairs of sender and receiver functions that handles data flow logics (
sendSomeAction()
andrecvSomeAction()
)
This chapter introduced how to use the ModulaJS Model API, and explained some necessary design details. For a complete references of the API, please refer to ModulaJS Model API.
The static factory method createModel(options)
is used to create a ModulaJS model class.
Assume you need to create a "todo item" model which contains a piece of data like { marked: false, text: 'some text' }
. We want to make sure marked
is always a boolean and text
is always a string. We can use propTypes
, which you already got familiar with when learning React. By default, marked
is false
and text
is undefined
, we can use defaults
to define these.
Once model class is defined, you may create its instances by using new
keyword and a props object (its keys/values should match propTypes
) as parameter.
import { createModel } from 'modulajs';
import PropTypes from 'prop-types';
const TodoModel = createModel({
displayName: 'TodoModel',
propTypes: {
text: PropTypes.string,
marked: PropTypes.bool.isRequired
},
defaults: {
text: undefined,
marked: false
}
});
const todo = new TodoModel({ text: 123 }); // Error: text is expected to be a string
const todo = new TodoModel({ text: '123' }); // good
// models come with some default methods like `get`
todo.get('marked') // false
todo.get('text') // '123'
Unlike many other model implementations, in ModulaJS models will refuse to take in "unexpected" data. For example:
const todo = new TodoModel({ text: 'some text', createAt: 1463691281790 });
todo.get('createAt') // undefined
So if you need any additional data, you need to define it in propTypes
first.
When defining defaults
in Model, please make sure to use functions to define non-primitive defaults values, so those functions only executed when the Model is initialized.
// GOOD
defaults: {
// `list` is initialized when Model instantiation, so that every new Model
// instance will have a new instance of `List`.
list: () => new List(),
name: "model"
}
// BAD
defaults: {
// `list` is initialized before Model instantiation, so the value will be
// shared by all instances of current Model, which may cause unpredictable
// issues afterward.
list: new List(),
name: "model"
}
It's recommended to provide a displayName
for a Model, which allows ModulaJS to build friendly error messages.
You can provide some extra getter and setter methods to make the Model easier to use. In the following example, we provide a setter function mark
to toggle marked
, and a getter function isValid
to check if the text is undefined.
Please note that any setter function should always return a new instance, just like set
.
import { createModel, PropTypes } from 'modulajs';
const TodoModel = createModel({
displayName: 'TodoModel',
propTypes: {
text: PropTypes.string,
marked: PropTypes.bool.isRequired
},
defaults: {
text: undefined,
marked: false
},
mark() {
return this.set('marked', !this.get('marked'));
},
isValid() {
return this.get('text') !== undefined;
}
});
const todo = new TodoModel();
const newTodo = todo.mark();
newTodo.get('marked') // true
A model comes with a set
function, with which you can mutate the data in the model. However, models are immutable, so set
will always return a new instance, and the old instance remains the same.
const todo = new TodoModel();
const newTodo = todo.set('marked', true);
newTodo.get('marked') // true
todo.get('marked') // false
todo === newTodo // false
There are other APIs to mutate the immutable model:
get(key)
likeImmutable.get
.getIn(path)
likeImmutable.getIn
.updateIn(path)
likeImmutable.updateIn
. Returns a new instance of Model.set(key, value)
likeImmutable.set
. value can be a function. Returns a new instance of Model.setMulti({ key: value })
set multiple key-values at once. value can be a function. Returns a new instance of Model.attributes()
returns an object with all property-values.toJS()
returns an plain object representation of a model, all child model will be converted to plain object as well.
One model is not enough to express all your data. Naturally you will want to nest your models, just like how you nest up your components. ModulaJS is designed in a way that you can easily manage deep nested models and localize data for every component.
The following example is a TodosModel
which contains a list of TodoModel
.
import { List } from 'immutable';
import { createModel } from 'modulajs';
import ImmutablePropTypes from 'react-immutable-proptypes';
import TodoModel from './todo_model.js';
const TodosModel = createModel({
displayName: 'TodosModel',
propTypes: {
todos: ImmutablePropTypes.listOf(PropTypes.instanceOf(TodoModel)).isRequired
},
defaults: {
todos: new List()
},
addTodo(text) {
return this.set('todos', (todos) => {
return todos.unshift(new TodoModel({ text }));
});
},
markAll() {
return this.set('todos', (todos) => {
return todos.map((todo) => {
return todo.set('marked', true);
});
});
}
});
TodosModel.fromJS = function(definition) {
const todos = definition.todos || [];
return new TodosModel({
todos: new List(map(todos, (todo) => new TodoModel(todo)))
});
};
In above example code, TodoModel.fromJS(definition)
method is not provide in ModulaJS Model API, but it's strongly recommended to add this static method to your model class if the model contains a hierarchy.
In business module or library component development, the model usually forms a model tree.
In ModulaJS model, a Context is a special prop, that is shared with all descendant models. It has a similar design to the Context of React. The context is useful to avoid passing a prop down manually level by level. The following example demonstrates who to define and use context.
const GridModel = createModel({
displayName: 'GridModel',
propTypes: {
table: PropTypes.instanceOf(TableModel)).isRequired
},
childContextTypes: {
someContext: PropTypes.string
},
getChildContext() {
return {
someContext: 'some text'
};
}
});
const TableModel = createModel({
propTypes: {
rows: PropTypes.instanceOf(TableRowsModel)).isRequired
}
});
const TableRowsModel = createModel({
// ...
contextTypes: {
someContext: PropTypes.string.isRequired
},
someMethod() {
const someContext = this.getContext('someContext'); // 'some text'
}
});
A context can also be a function (PropTypes.func
), so that the descendant model can invoke methods defined in an ancestor model. But please note that, the function-as-context feature is usually used to pass some utility functions, DO NOT abuse it in other use cases. For example, if a descendant model would like to mutate ancestor model, do not pass set()
method as context, but leverage Sender and Receiver mechanism that will be introduced in next section.
Sometimes you define some convenient instance methods in a model, then add the model as a child of a parent model, and you need to "expose" those methods in parent model. It's possible to add a wrapper method:
const GridModel = createModel({
displayName: 'GridModel',
propTypes: {
table: PropTypes.instanceOf(TableModel)).isRequired
},
getRowIds() {
return this.get('table').getRowIds();
},
getSelectedRowIds() {
return this.get('table').getSelectedRowIds();
}
});
Think of the situation that you have many methods to expose. There is a better way to achieve this, which is delegates
option:
const GridModel = createModel({
propTypes: {
table: PropTypes.instanceOf(TableModel)).isRequired
},
delegates: {
table: [
{ method: 'getRowIds' },
{ method: 'getSelectedRowIds' }
]
}
});
The delegates
way is recommended for most Method Delegation cases.