Then, we add the remaining local nodes: FeedingTheBunny and Thanks in the same way.

Now it’s time to add arrows. In order to do it, we need to put the cursor over some node, aim the square handler displayed on it and drag a line from it to another node.

At this point, the newly created arrow can be customized. All we need to do is to click it and then enter its new name. This one is going to be called has a bunny and will tell us what happens when the user has a bunny — she goes from the home screen to the screen where she’s supposed to enter her name.

Giving a name to the edge from the IsABunnyOwner node

Let’s draw all the remaining arrows and call them after events which cause behavior changes.

All main edges are drawn

This graph isn’t finished yet. The whiteboard drawing wasn’t perfectly detailed. There are at least two missing elements:

the initial node

underlaying nodes

We need to specify that this graph starts from the IsABunnyOwner node. All we need to do is to draw an arrow from the start entry point to the IsABunnyOwner node.

The main graph got an initial node

Selecting underlaying nodes means specifying which graph node actually lays under a local graph node. For example, there’s the IsABunnyOwner local node. But what’s its type? Is this a leaf? Or maybe it’s a subgraph? In order to make it clear, we need to add an actual node and wire it with the local node. It may sound a bit complicated, but it’s actually pretty simple. Here’s how we do it.

First, we need to create the actual node. We do it exactly in the same way the main node was created, because main is an actual node as well.

Adding a new actual node — IsABunnyOwner

Then, we go back to the main node, select the IsABunnyOwner local node and select the IsABunnyOwner actual node as its underlaying node. That’s it!

The Thanks node is a leaf as well. We add and wire it in the same way as we did it with the IsABunnyOwner node.

Thanks added and wired

Nodes EnteringTheName #1 and EnteringTheName #2 are a bit more interesting, because even though they are two separated local nodes from the point of view of the main node, they utilize the exact same actual node as their underlaying nodes. That’s why we add just one actual node EnteringTheName.

Adding EnteringTheName

Then we pick it as the underlaying node for both EnteringTheName #1 and EnteringTheName #2.

The EnteringTheName #1 local node uses the EnteringTheName actual node as its underlaying node

The EnteringTheName #2 local node uses the EnteringTheName actual node as its underlaying node

FeedingTheBunny is going to be far more complicated. Here’s how we decided that it has two regions:

the bunny itself

controls for carrots (looking for carrots and giving them to the bunny)

A whiteboard drawing of FeedingTheBunny

It makes it a composite with two children.

Adding the FeedingTheBunny composite

The FeedingTheBunny composite with its children

Let’s start from the most important part — the bunny. It can be:

hungry

eating

full

Here’s how does TheBunny look.

TheBunny graph

We cannot forget about underlaying nodes. There are three of them:

AHungryBunny

AnEatingBunny

AFullBunny

They are simple enough to be leaves. We add them in the same way how we added other leaves.

This is how CarrotControls look in the editor.

CarrotControls graph

Both LookingForACarrot and GivingTheCarrot are leaves. They must be added and have their underlaying nodes selected.

At this point, all nodes and arrows are drawn. We’re one click away from generating the src/graph.json file! All we need to do is to click the GENERATE CODE button.

Generated code

The generated code should be saved as src/graph.json.

If for some reason you get an error saying that the code cannot be generated, please make sure that all local nodes have their underlaying nodes selected.

Coding different behaviors

After having some (I hope) good time drawing, it’s time to code a bit. We’re going to start from the handler of the IsABunnyOwner node. The first step is to create a handler file, which in this case is called IsABunnyOwner.js and it’s located in the handlers directory.

A quick look at its render method reveals that it gives the user a choice by displaying two buttons. Let’s make sure it actually renders without crashing. We will need a test for this handler. All we need to do is to create a file called IsABunnyOwner.test.js which is located in the same directory.

Now, when we run the following command, we can make sure that it actually renders two buttons.

npm test

The graph tells us that we expect the IsABunnyOwner node to follow either has no bunny or has a bunny arrow. Here’s the complete handler code.

The render method takes a reference to the model object. It can be used to build event handlers which call methods of the Rosmaro model. In this case, the same handler that renders the UI, also handles method calls. That’s why there are functions hasABunny and hasNoBunny. All they need to do is to return the arrow we want the model to follow when they’re called. If you want, you can find more about this in the documentation chapter about method handlers.

And here is the test case. Please notice how Sinon spy objects are used to make sure that clicking a button actually triggers a model method.

When it comes to the home screen of our application, the only thing left to do is to wire the handler with the graph node. At the beginning of this article we created the src/handlers/all.js file. All it’s responsible for is to provide a map connecting graph nodes with their handlers. This is how does it look with the first handler.

And that’s how we finished the home screen of our app.

The node meant to enter the name, that is EnteringTheName, behaves slightly different. It not only follows arrows, but also alters the context of the Rosmaro model.

You can think of context as of something similar to state from React or store from Redux.

In order to set the default value of the user name, we’re going to wire a simple handler to the main node.

The list of all handlers must be updated.

Since now, every time a child of the main node reads ctx.name before the context is set to any non-empty value, it’s going to read Unknown person.

The EnteringTheName handler renders a text field and a button.

Please note how we read the name property of the context. The method is called with an object. It has a property called ctx, which is the context. We read it using object destructing.

We want to make sure the view renders without crashing and that it sets the value of the text field to the name read from the context.

Adding the behavior responsible for handling the Done button is very similar to what we did in order to handle buttons rendered by the IsABunnyOwner node.

To sum it up, in order to handle a button click that makes the model follow an arrow, we need to:

register an event listener that calls a model method

create a model method that follows an arrow

The thing that makes the EnteringTheName node more interesting than other nodes with buttons is that it handles typing on the keyboard as well. We want to let the user type her name, what means altering the name context parameter. The only way to update the context is by doing a transition. That’s why we’re going to need an extra arrow — to handle a situation when the name is being typed but it’s not ready yet so we don’t want to change the screen.

Loops called “typed” for “EnteringTheName” nodes

Now we can register an event listener that will call a model method meant to do a transition and set the new value of the name.

The most common mistake I make writing handlers like this is to replace the whole context instead of updating just one field. Let’s take a look at its test case.

If you were wondering what’s the point of the {another: 123} part of the context, this is the answer — to make sure it’s not accidentally removed. This handler is meant to update the name property. It shouldn’t touch other properties.

There’s one more thing about the EnteringTheName node I’d like to mention about. Let’s have a look at the main graph one more time.

A whiteboard drawing of the most important screens

As we can see, when the name is entered, the user lands on either the FeedingTheBunny screen or the Thanks screen. What’s cool is that the code responsible for handling the Done button always does the same — it makes the model follow an arrow called done. There is no IF for that. It’s not parametrized using a boolean flag neither. It’s all about where is the arrow pointing at.

We’re getting closer to the most complicated part which is the FeedingTheBunny node and its children. The node itself is a composite of two nodes: TheBunny and CarrotControls. Both of those nodes handle the render method in their own ways. In order to make the composite render correctly, we need to make its render method return a merged result of the _render_ method of TheBunny and the render method of CarrotControls. This is how we can use altering results to achieve this.

This is its test case.

We’re going to start from the CarrotControls graph. What makes it special is that it calls model methods which are handled by two handlers:

the controls themselves (when we give a carrot away, we don’t have it anymore)

the bunny (when the bunny is given a carrot, it’s not hungry anymore)

First, let’s do the controls.

These buttons simply change between Look for a carrot and Give the carrot. They do it by calling model methods like giveTheCarrot. And I must tell you a secret — TheBunny is especially interested in getting a carrot.

To make TheBunny children a bit simpler, we’re going to give them just a slice of the whole context. The whole context looks like this.

But we want TheBunny and its children to see just this.

It turns out to be very easy with context slices. All we need to do is to write a simple handler for TheBunny.

Now we can create AHungryBunny. It will handle the same method call which makes the controls change from GiveTheCarrot to LookingForACarrot, that is giveTheCarrot.

Every time we call the giveTheCarrot method, the node called GivingTheCarrot follows the arrow called gave the carrot and the node called AHungryBunny follows the arrow called ate a carrot (and sets the number of ate carrots to 1).

When AHungryBunny eats a carrot it becomes AnEatingBunny.

Every time it’s given a carrot, it increments the number of ate carrots by 1 (what’s actually done by returning a new version of the context, very much like in Redux). What makes it different than any other handler we implemented is that it picks the arrow to follow based upon the context of the model. If the bunny has eaten at least 5 carrots, it follows the ate 5 carrots arrow. However, if the bunny ate less than 5 carrots, it just updates the number of ate carrots. It accomplishes this by following a different arrow called ate a carrot. It is very much like the EnteringTheName node which follows the typed arrow in order to update the context. That’s why we need to add this arrow to the graph.

TheBunny and “ate a carrot” arrow

Finally, AFullBunny is happy and allows us to go back to what we were doing.

The last screen is called Thanks and it simply says thank you and lets us to start the whole process one more time.

The application is almost ready. The missing functionality is about TheBunny being hungry every time we see it, while the state of CarrotControls should never change without user interaction. It means that if we leave the bunny full and we have a carrot (let’s imagine we’re carrying it in a bag), then the bunny is hungry when we meet it again but we don’t need to look for another carrot because we already have one (the one we found previously).

Implementing this feature is actually quite easy and can be done just by editing the graph. First, let’s use a special entry point to FeedingTheBunny. We’re going to call it feeding. All we need to do is to open the main graph, click the fed the bunny arrow and change its entry point from start to feeding.

Renaming the entry point to “feeding”

Since now, every subgraph of the FeedingTheBunny must have an entry point called feeding. So let’s start with TheBunny, which is always hungry when it comes to feeding it.

Open TheBunny Click NEW ENTRY POINT Type feeding as its name Click ADD Draw an arrow from the newly created feeding entry point to AHungryBunny

“feeding” entry point to TheBunny

In order to make the CarrotControls remember the recently active node, we’re going to make use of the special recent node. It symbolizes the node which was active before the graph has been left, or if it has never been active, then it’s the node the arrow from the start entry point is pointing at. Long story short — we need to add the feeding entry point and connect it with the special recent node.

“feeding” entry point to CarrotControls

And that’s pretty much it! The application works as expected and consists of just one IF statement. What’s worth noticing, this IF statement is not mandatory. It could be totally removed, what would make the codebase free of any IF statements, if the whole memory of the Rosmaro model was expressed using graph nodes. However, it would lead to a phenomenon called state explosion. This single IF statement turns out to be surprisingly helpful! It’s one of the the tools which allow us to mitigate the risk of ending up with an enormous number of nodes and arrows, together with:

composites (orthogonal regions)

subgraphs (state machines in state machines)

context (a piece of data usually in the form of a map structure)

Depending on our needs, we can balance the number of nodes and IFs. There may be just one node and many IFs, or many nodes and no IFs at all.

The full source code of the application we’ve just built is available on GitHub — Rosmaro-React-example-Bunny-App.

Modifications worth trying out

Here are few modifications which I think are worth trying out. It doesn’t mean that they represent a better approach. They’re simply interesting exercises:

removing all IF statements from method handlers (hint: it will increase the number of graph nodes) making the typed arrow disappear from the main graph (hint: a new subgraph will be necessary) adding a back button from Thanks (hint: depending on the destination screen, new nodes may be necessary)

Does it scale?

I often hear, that finite state-machines don’t scale. I totally agree on that, when it comes to simple, pure FSM-based solutions. When the graph is the only memory and when every single event needs to be explicitly represented by its own arrow, even trivial tasks may lead to an enormous number of nodes and arrows.

A great example to illustrate this problem is a Hello YourName! application, which has one screen with a text input and a button and another screen to display greetings. Even if we limit the length of the name to 2 characters and allow only two letters: a and b, the graph will still look like this.

An example of the phenomenon called state explosion

This doesn’t look good.

However, there’s no reason why we need to limit ourselves to just one-level graphs or even graphs at all. Thanks to the fact that Rosmaro is not a pure FSM-based solution, we can use some of the techniques described above to make the graph look exactly how we want it to look. We could even reduce the number of nodes to exactly one by replacing other nodes with IFs. Of course, it wouldn’t make to much sense to use Rosmaro if we needed just one node. It simply illustrates the possibility of balancing between nodes and IFs in order to achieve a structure that represents the intended behavior of the application in a clear, documentation-like manner.

Here’s the Hello YourName! application modeled using method handlers, context and subgraphs.

The main graph of the Hello YourName! application

The subgraph of the Hello YourName! application

It doesn’t look so scary, does it?

And when it comes to a bit more complex examples, here’s an application with 13 different screens which depend on each other and many paths the user may take.

A more complex example

In the code, there are 3 IF statements. The graph consists of 29 nodes.

The main node opened in the editor looks like this.

The main node of the complex example

Conclusions

To me, visual automata-based programming turned out to be a really great discovery. For a long time, I considered state machines something almost purely theoretical. Or at least not directly applicable to the field I was working on.

But then I imagined the code structured like there would be islands of clearly defined behavior. These islands are handlers from Rosmaro. There are also bridges connecting these islands. They are arrows representing events. The current behavior of the system is changed by going from one island to another. It all clicked. Now I can design and implement changes of behavior in an elegant, organized way, where previously I would just scatter IF statements.

I can also see how I benefited from automata-based programming in two different ways. It’s not only easier to program by drawing boxes and arrows instead of inspecting boolean flags. It also makes it necessary to discover and name all the important states of the application, what’s a great framework for thoughts.

And when it comes to application state, it showed me that there’s state management beyond map structures (which are key-value pairs, like method-less JavaScript objects). A new tool appeared in my toolbox. It’s the state machine based dispatch mechanism. Instead of just accepting the fact that I have some boolean values to check using IFs, I can consider replacing them with nodes. After playing a bit with this approach, I started noticing the difference between data-related state and behavior-related state. Data-related state is something like information about a user. It may be as simple as name and surname. Most of the time, we don’t need to use IFs to deal with state like this. It’s simply there. We can display or save it without conditional statements. A set of key-value pairs is a perfect model for state like this. On the other hand, behavior-related state determines how should the application work according to events which occurred in the past. Sometimes key-value pairs topped with a few IFs do the job very well, other times state machines feel a lot more natural and easy to understand.

If you haven’t tried yet, I hope you’ll give state machines a go. Maybe you’ll also find them valuable in your day to day job?