Meiosis Documentation

Table of Contents

SAM Pattern

The SAM (State-Action-Model) Pattern, by Jean-Jacques Dubray, is a way to structure code into these parts:

You can find more details and explanations on the SAM web site.

As you can see, the basis of Meiosis is similar to SAM. The SAM State corresponds to Computed properties, and the Next-Action-Predicate corresponds to services, as we saw in Using Computed State and Services.

SAM goes one step further with the concept of presenting values and accepting or rejecting them. This is essentially the accumulator function of Meiosis, but it does not automatically apply incoming patches.

Let's see how we can use the Meiosis pattern as a foundation and augment it to add these concepts of the SAM pattern.

Meiosis Pattern

Remember the fundamental Meiosis Pattern:

const update = flyd.stream();
const states = flyd.scan(P, app.initialState(), update);

Actions send patches in the form of objects or functions to the update stream. Using scan and an accumulator function, we produce a stream of states. We can then use the view library of our choice, passing the current state and the actions to the view.

In Using Computed State and Services, we added computed and services:

const computed = state =>
  computes.reduce((x, f) => P(x, f(x)), state);

const states = flyd.scan(P, app.initialState(), update)
  .map(computed);

states.map(state =>
  services.forEach(service => service(state, update)));

Let's look at how we can use Meiosis as a basis to implement the SAM pattern.

A Navigation Example

Let's look at an example. Say we have navigation between different pages. Clicking on a section of the navigation bar shows the corresponding page. To navigate, we have actions that update the model to indicate the current page. The view uses the model to render the corresponding page.

The example is below. Notice how you can go to different pages; Logout sends you back to Home; and the Data page has no data to show, so it displays a Loading, please wait... message.

Let's see how we can apply the SAM pattern.

Using update as present

In SAM, actions present values to the model, and the model's acceptor function decides if and how to update the model. The update stream from Meiosis can be used as a present function for actions.

const present = flyd.stream();
const actions = app.actions(present);
const states = flyd.scan(P, app.initialState(), update);

This is just a name change, but it conveys the role of present in the SAM pattern.

Acceptor

Next, SAM has the concept of an acceptor function in the model which receives the values presented by actions and updates the model accordingly. In Meiosis, the acceptor function is the accumulator function that we use with scan. Right now this is Patchinko's P function, which merges changes into the model. Let's separate it out into an explicit acceptor function:

const acceptor = (model, proposal) => {
  return P(model, proposal);
};

The values presented by actions are proposals. The acceptor function gets the latest model and the proposal, and decides how to update the model.

Because we are using objects as proposals, the acceptor function can examine the proposal and conditionally refuse, partially accept, or otherwise change how the model gets updated. In the example below, the action presents a value to navigate to a Settings page. But if the user is not logged in -- the user is not in the model -- the acceptor navigates to the Login page instead:

const acceptor = (model, proposal) => {
  if (proposal.pageId === "SettingsPage" && !model.user) {
    return P(model, { pageId: "LoginPage" });
  }
  return P(model, proposal);
};

Note that we are able to look at proposals because they are in the form of objects. We couldn't do that if we were using functions instead. Patchinko comes in handy here to manage our model updates.

Now our setup is:

const acceptor = (model, proposal) => { /* ... */ };
const present = flyd.stream();
const actions = app.actions(present);
const states = flyd.scan(P, app.initialState(), update);

We can use the acceptor to guard against going to the Settings page without logging in. Below, try it out:

State

The State function looks at the model and makes any changes necessary to produce application state that is suitable for the view.

For example, say we want to prepare a login form by initializing the username and password to blank strings. Further, we want to clear out those values when navigating away from the login page:

const prepareLogin = model => {
  if (model.pageId === LoginPage && !model.login) {
    return { login: { username: "", password: "" } };
  }
  else if (model.pageId !== LoginPage && model.login) {
    return { login: null };
  }
};

As a convenience, if the user clicks on Settings without logging in, we want to return to the Settings page after they have logged in, since that is where they were trying to go. We can use a returnTo property to indicate this, and make sure we clear it out after using it:

const checkReturnTo = model => {
  if (model.user && model.returnTo) {
    return { pageId: model.returnTo, returnTo: O };
  }
  else if (model.pageId !== LoginPage && model.returnTo) {
    return { returnTo: O };
  }
};

These two functions, prepareLogin and checkReturnTo, are State functions. They look at the model, and decide whether to return changes that will produce the application state. We can combine them into a state function:

state: model => [
  prepareLogin,
  checkReturnTo
].reduce((x, f) => P(x, f(x)), model)

Now our setup is:

const present = flyd.stream();
const actions = app.actions(present);
const states = flyd.scan(app.acceptor, app.initialState(), present)
  .map(app.state);

Try it out below. Now you are sent to the Settings page after logging in, if you had previously attempted to go to Settings. Also notice that if you go back to the Login page, the form is now cleared out.

Next-Action-Predicate

The final part of the SAM pattern is the Next-Action-Predicate (nap). This is a function that looks at the application state and decides whether to automatically trigger another action (the next action).

Let's use this to load the data on the Data page. We want the action to complete -- navigating to the Data page and showing the please wait message -- but then if there is no data in the application state, we want to automatically trigger the action that loads the data:

nap: actions => state => {
  if (state.pageId === "DataPage" && !state.data) {
    actions.loadData();
  }
}

Our setup becomes:

const present = flyd.stream();
const actions = app.actions(present);
const states = flyd.scan(app.acceptor, app.initialState(), present)
  .map(app.state);
states.map(app.nap(actions));

Now if you go to the Data page, you will see the please wait message for a couple of seconds, and then a message saying The data has been loaded. If you navigate away and then come back to the Data page, the data is still there. But if you click on Logout, the data is cleared out of the application state.

Try it out:

With present, acceptor, state, and nap, we have used Meiosis as a foundation and implemented SAM.

Table of Contents


Meiosis is developed by @foxdonut00 / foxdonut and is released under the MIT license.