ic_fluent_resize_20_filledCreated with Sketch.
ic_fluent_resize_20_filledCreated with Sketch.
ic_fluent_resize_20_filledCreated with Sketch.
ic_fluent_resize_20_filledCreated with Sketch.
ic_fluent_resize_20_filledCreated with Sketch.
ic_fluent_resize_20_filledCreated with Sketch.
ic_fluent_resize_20_filledCreated with Sketch.
ic_fluent_resize_20_filledCreated with Sketch.
ic_fluent_resize_20_filledCreated with Sketch.
ic_fluent_resize_20_filledCreated with Sketch.
ic_fluent_resize_20_filledCreated with Sketch.
Skip to content

Yantrix

Yantrix is a TypeScript framework that provides a set of tools to create robust and self-documented functional applications by code generation. The business logic is represented by declarative, event-driven finite state machines, while the application state is an Anemic Domain Model, making it great a counterpart to any traditional state manager like Redux, while allowing devs to focus on describing contracts and workflows, rather than writing and debugging the actual code.

Lends itself perfectly to Architecture-as-Code paradigm and no-code/less-code tools for developers, like n8n.

Quick Start

Let's build a slider component. First, we need a diagram for a State Machine describing its behaviour. The diagram is written in Yantrix subsyntax of Mermaid and can be visualized directly with that awesome library. Click on <> button to see the source code:

ic_fluent_resize_20_filledCreated with Sketch.

The details of what's going on here are explained below. For now, assume it's saved to a file named slider.mermaid. Then, we pass that source to a code generator and choose a language we need

shell
$ yantrix codegen ./slider.mermaid --outfile slider_controller.js --language Javascript --className Slider

Now, we import the generated file into your projects:

javascript
import Slider from './slider_controller.js';

const SLIDER_MAX = 1000;
const SLIDER_MIN = -1000;

const SliderController = new Slider();

SliderController.dispatch(
	Slider.createAction('RESET', {
		min: SLIDER_MIN,
		max: SLIDER_MAX,
	}),
); // adjusted to local needs

/*
 * ...somewhere later on at UI ...
 */

// bind these to buttons and/or keys
const rightHandler = () => SliderController.dispatch(Slider.createAction('INCREASE', { by: 50 }));
const leftHandler = () => SliderController.dispatch(Slider.createAction('DECREASE', { by: 50 }));

// bind this to a click handler, using relative coordinate
const setHandler = (percentage) =>
	SliderController.dispatch(
		Slider.createAction('SET', {
			value: SLIDER_MIN + percentage * (SLIDER_MAX - SLIDER_MIN),
		}),
	);

// use this to display value in the GUI
const getSliderValue = () => SliderController.getContext().value;

// and this - to prevent UI from changing the slider value
const isDisabled = () => Slider.hasState(SliderConroller, 'DISABLED');

Or you can use one of the available Integrations with a framework of your choice, i.e. Redux

Syntax Breakdown

Let's see what's happening in depth. The diagram describes an FSM, — a finite state machine, a control object that is characterized by a limited set of States, and it always resides in one and only one of them.

States

Every State is independent on another and difference between States is qualitative, i.e. they represent some scenarios in App that are independent on each other and never happen simultaneously:

  • Slider is usable and idle (enabled)
  • Slider can't be interacted with (disabled)
  • Slider is being switched to a particular position
  • Slider is being nudged left or right
ic_fluent_resize_20_filledCreated with Sketch.

Actions

To change the State of the machine, an Action must be invoked. Every Action has a unique name, and the first layer of diagram is essentially a graph that depicts which Actions lead to which States.

ic_fluent_resize_20_filledCreated with Sketch.

Also note that not every Action can be invoked from every State by default. A [*] node is used to define an Action that can be invoked from every State. The binding between States and Action define a many-to-many relation, called Transition Matrix, that is another way to define the FSM. Every item in this matrix is either null or an Action name, while columns and rows correspond to States in the same order.

From/ToENABLEDDISABLED
anyRESET
ENABLEDDISABLE
DISABLEDENABLE

Payload

Every Action can carry a Payload, which is a plain data object that represents some meta-information about the change requested by it. For instance, when resetting (instantiating) a Slider, it's wise to provide its min and max values. Payload is represented with a list of parameters in parentheses, each possible having a default value:

ic_fluent_resize_20_filledCreated with Sketch.

Technically, nudging Slider can be processed the same way as setting it directly, by an only Action, carrying a polymoprhic Payload, allowing to either adjust value or to set it directly:

ic_fluent_resize_20_filledCreated with Sketch.

However, different behaviours are better to be represented with different States.

Intermediary States

While FSM is transactional and synchronous (i.e. no further computation is done until the invoked Action has been processed), most interactions are asynchronous by nature, and they have some in-between state, usually called "pending" in this context.

In fact, even synchronous operations could have an intermediate State which actually does computations, and this would be an advised design pattern. As you can guess, those operations then could be easily plugged in with async functionality, behaving as a Promise.

ic_fluent_resize_20_filledCreated with Sketch.

here we define 3 Actions and 3 corresponding intermediate States for them:

  • SET (value) – a direct assignment of the Slider value, leading to SETTING State and carrying new value in Payload
  • INCREASE (by) – an increment of the Slider value, leading to INCREMENT State and carrying a difference in Payload
  • DECREASE (by) – a decrement of the Slider value, leading to DECREMENT State and carrying a difference in Payload

Here we use [-] labels on Actions, which is a special symbol for ByPass Action. Now, to emphasize that we need to add Notes to States to describe their behaviour:

ic_fluent_resize_20_filledCreated with Sketch.

Our Transition Matrix now looks like this:

From/ToENABLEDDISABLEDINCREMENTDECREMENTSETTING
anyRESET
ENABLEDDISABLEINCREASEDECREASESET
DISABLEDENABLE
INCREMENTINCREASE
DECREMENTDECREASE
SETTINGSET

Context

Now, we need to define Context — values that are stored within FSM along with the current State and represent some of its quantitative properties. We define it with Reducers along with State, and they are calculated whenever the FSM is switched to that State. The Reducers defined at [*] node are run on every transition, and the Context properties found there are persistent through every Action, unless specified otherwise.

ic_fluent_resize_20_filledCreated with Sketch.

here we have defined 3 Context properties, that are copied from the previous values with every Action:

  • value stores current Slider position
  • min stores minimal selectable value
  • max stores maximal selectable value

Initial Context

When RESET Action is invoked, a new min and max values can be set with Payload. Since that Action resolves to ENABLED State, that's where the related Reducer belongs:

ic_fluent_resize_20_filledCreated with Sketch.

Here we have also specified that ENABLED is the starting State of the FSM with Init flag.

Operations on data

Now we specify the arithmetics for intermediate "nudge" States INCREMENT and DECREMENT, which will add or substract, respectively, the provided Payload property by to/from stored value in Context :

ic_fluent_resize_20_filledCreated with Sketch.

Note that we use built-in Transformer Functions min,max,add and diff here in such a manner, that the resulting value always stays within min and max bounds.

Finally, we write a simple assignment Reducer for SETTING intermediate State, that will copy value from Paylod to Context, and we are done:

ic_fluent_resize_20_filledCreated with Sketch.

Now, it's only about dispatching proper Payloads to the FSM.

What's next?