Sometimes a model need to talk to another model. For complicated states, they probably communicate even more often. ModulaJS model provides several mechanisms to handle the different communication directions.
If the other model is a descendant, we could use one of those 2 methods to communicate.
We could mutate the child directly, which require the child to provider some mutation methods:
const ParentModel = createModel({
displayName: 'ParentModel',
propTypes: {
content: PropTypes.string,
child: PropTypes.instanceOf(ChildModel)
},
sendClear() {
this.dispatch({ type: ActionTypes.CLEAR });
},
recvClear() {
return {
type: ActionTypes.CLEAR,
update(model) {
const newModel = model.setMulti({
content: '',
// the clear method is a mutation method provided by child model
// which will return a 'cleared' child model
child: c => c.clear()
});
return [ newModel ];
}
};
}
});
The pro for this pattern is that the operation is 'atom'.
Or we could use SideEffect + Method Delegation to solve the problem.
const ParentModel = createModel({
displayName: 'ParentModel',
propTypes: {
content: PropTypes.string,
child: PropTypes.instanceOf(ChildModel)
},
delegates: {
child: [
{ method: 'sendClear', as: 'sendChildClear' }
]
},
sendClear() {
this.dispatch({ type: ActionTypes.CLEAR });
},
recvClear() {
return {
type: ActionTypes.CLEAR,
update(model) {
const newModel = model.set('content', '');
return [
newModel,
// this additional side effect will dispatch another action to clear child
newModel.sendChildClear
];
}
};
}
});
The difference is that call child sender will dispatch another action and will trigger view rendering twice.
If the other model is a ancestor, we also have two ways to handle the communication.
As the name indicted, a model fires an Event with a specific type
, bubbles to its parent model and all the way to the root model, any model in the path that watches the event of specific type
will get notified when event arrives.
Payload can be passed up to those events watchers by setting the second argument for bubbleEvent
calls and data types in payload also have to be declared both in eventTypes and watchEventTypes. Similar to PropTypes
, payload data should be Immutable
if necessary.
const ParentModel = createModel({
displayName: 'ParentModel',
propTypes: {
child: PropTypes.instanceOf(ChildModel)).isRequired
},
watchEventTypes: [
'childUpdated',
{
type: 'refreshHeader',
payload: {
ids: ImmutablePropTypes.listOf(PropTypes.number),
refreshImmediately: PropTypes.bool
}
}
],
watchEvent(type, from, payload) {
if (type === 'childUpdated' && from('ChildModel')) {
// Do something
} else if (type === 'refreshHeader' && from('ChildModel')) {
this.sendRefreshHeader(payload);
}
}
});
const ChildModel = createModel({
displayName: 'ChildModel',
eventTypes: [
'childUpdated',
{
type: 'refreshHeader',
payload: {
ids: ImmutablePropTypes.listOf(PropTypes.number),
refreshImmediately: PropTypes.bool
}
}
],
recvChange() {
return {
type: ActionTypes.CHILD_CHANGE,
update(model) {
const newModel = model.set('value', 'new');
return [
newModel,
newModel.bubbleChildUpdated
];
}
};
},
bubbleChildUpdated() {
this.bubbleEvent('refreshHeader', {
ids: new List([1, 2]),
refreshImmediately: true
});
}
});
Once child.bubbleChildUpdated()
is called, the event childUpdated
will be bubbled to parentModel
, then parentModel.watchEvent(type, from)
would be invoked to handle the event.
If there're multiple watching model in the ancestors path, all them will be notified, and the closer one will be notified first.
There's a special model life cycle hook called modelDidUpdate(oldModel, newModel)
.
An interesting fact of this life cycle hook is that any time when a child model is updated, the parentModel.modelDidUpdate will be called. That means we could utilize this life cycle to identify child changes.
const ParentModel = createModel({
displayName: 'ParentModel',
propTypes: {
otherAttribute: PropTypes.any,
child: PropTypes.instanceOf(ChildModel)).isRequired
},
modelDidUpdate(oldParentModel, newParentModel) {
const oldChild = oldParentModel.get('child');
const newChild = newParentModel.get('child');
if (oldChild !== newChild) {
// ok a child update is caught
if (oldChild.get('name') !== newChild.get('name')) {
// do something special
}
}
}
});
If two sibling models need to communicate with each other, their common parent needs to become the coordinator.
And then we can use the one of the above "Child-to-Parent" methods to let the parent know that something happens to one model, and the parent model could then use one of the "Parent-to-Child" methods to update the sibling model.
The model-to-view communication is quite straightforward. Taking React components as an example, a TodoModel instance is passed into TodoComponent as model
prop:
const TodoComponent = ({ model }) => (
<div>{ model.get('todoList').get(0) }</div>
);
TodoComponent.propTypes = {
model: PropTypes.instanceOf(TodoModel).isRequired
};
In contrast with above model-to-view communication, the view-to-model communication is relatively complex.
In ModulaJS framework, we introduce the Sender and Receiver pairs in models. A Sender is an instance method with "send" prefix, e.g. sendTodoAdd()
; it should always dispatch an Action with specific type
and payload
by calling this.dispatch(action)
. A Receiver is an instance method with "recv" prefix, and must be in a pair with corresponding Sender, e.g. recvInit()
; it should always return a object: {type: string, update: function(model, action)}
, the update() function returns a list of side effects and as a convention the first one must be the new model. An example as follows.
const ActionTypes = createConstants('TODO', {
ADD: null
});
const TodoModel = createModel({
displayName: 'TodoModel',
propTypes: {
todoList: ImmutablePropTypes.listOf(PropTypes.string)
},
sendAdd(todo) {
this.dispatch({
type: ActionTypes.ADD,
payload: { todo }
});
},
recvAdd() {
return {
type: ActionTypes.ADD,
update(model, action) {
const { todo } = action.payload;
const newModel = model.set('todoList', list => list.push(todo));
return [ newModel ];
}
};
}
});
TodoComponent could call model.sendAdd()
, then the ADD
action is dispatched and then be delegated to the recvAdd.update
, which returns a new model. Later TodoComponent gets notified and re-renders with the new model.