-
Notifications
You must be signed in to change notification settings - Fork 1
Tutorial: Building Tooling (pt. 1)
One of the areas where I want Machinate to shine is in having a rich ecosystem of tooling. Here, I'm going to take a stab at building some tooling (a tool I'll call Inspector), but I'll build that tool using Machinate as well.
This should be a fun experiment in dogfooding, and will put the framework fully to the test. In Machinate I trust!
I'll be developing on Codesandbox and will have snapshots of my progress available to follow along.
Let's start with a mockup and an empty Sandbox:
Let's first come up with a scheme. A scheme is a JS object defining the different "buckets of state" (aka. domains) in your app. So we'll begin by identifying these domains.
We quickly first notice there are 3 different sections in the layout:
- Active States / Watch (this section has to do with examining the state of the Machinate app)
- Externals / Blacklist (this section has to do with asynchronous/impure tasks triggered by a Machinate app)
- Transitions (this section has to do with overseeing/creating transitions that affect the Machinate app)
For items 1 and 2, we notice that only one is visible at a time. Let's model that.
const scheme = {
"StatesView": ["ActiveStates", "Watch"],
"ExternalsView": ["List", "Blacklist"],
"TransitionsView": ["List"]
}
Some Machinate terminology: the keys in the object above are called the "domain" and the values in the array are called "states". A domain contains one or more states that are orthogonal, meaning they are mutually exclusive. Only one state per domain can be active at a time. A domain either has one active state, or the domain does not exist at all.
Naming these is kind of subjective. Defining them is also a bit of an art, not unlike deciding what is a Component in your React app and what isn't. Here I choose to give Transitions just a single state... I wasn't sure what to call that single State since it doesn't really change, I'm trying out calling those types of states either "Value" or "State". Here it seems like State works.
While we're at it, let's also quickly set up the initial values for the domain. Your app always needs an initial state to start off at. This initial state describes what domains to include initially, and what state each of them start off at. Based on our mockup, it looks like "ActiveStates" and "List" are shown by default for StatesView and ExternalsView respectively. Let's code it up:
const initial = {
"StatesView": "ActiveStates",
"ExternalsView": "List",
"TransitionsView": "List"
}
Simple enough, no? Now let's get to the fun stuff.
First we'll add machinate
to the App:
yarn add machinate
and set it up in our app:
import { Machinate, States } from "machinate";
const App = () => (
<Machinate scheme={scheme} initial={initial}>
<States of="StatesView"
ActiveStates={() => <div>Active States</div>}
Watch={() => <div>Watch</div>}
/>
</Machinate>
)
Here we tell machinate via props what the scheme for our app is and where to start off the states for the app.
We'll also use the <States />
here to unlock the full power of machinate. Get used to this simple component as you'll be using it a lot.
With the <States />
component, you provide an of
property that refers to a domain. Then we will define what the view should look like for each of the possible states of that domain, in this case StatesView. StatesView has two different possible states, "ActiveStates" and "Watch". This means that our component must implement two props for each of these states. The props are named after the state. If you leave one out, machinate will warn you at runtime:
This is good practice and enforces you to consider all parts of your applications states and describe behavior for all of them.
Here is what we have so far: Sandbox: Basic set up. Notice how Machinate automatically displays what's in the ActiveStates prop since we set that in the initial state. Try playing around with the initial state and changing it to StatesView: "Watch",
. Note how the app automatically reflects that change when it reloads.
Here is machinate warning you if you leave one of the states out: Sandbox: Missing state prop
I used styled-components to spruce up the app a bit and make it look closer to the mockups:
Everything is still in the same single index.html file now and we have a lot of duplicated code with the boilerplate for Panes. Let's refactor a bit.
Cool, our index.js looks much slimmer and pertinent now! The new <Pane />
component will abstract the design details away 👍
First, let's get our tabs to light up when their state is active. Conveniently, Machinate provides an IsActive render prop component.
We'll update our Pane.js to wrap our <NavItem />
with the <IsActive />
component, and use the active
render prop to toggle a className of .active
.
// BEFORE:
<PaneNav>
{navigation.map(item => (
<NavItem key={item.text}>{item.text}</NavItem>
))}
</PaneNav>
// AFTER:
import { IsActive } from "machinate";
<PaneNav>
{navigation.map(item => (
<IsActive state={item.state}>
{ active => (
<NavItem className={active? "active" : ""} key={item.text}>
{item.text}
</NavItem>
)}
</IsActive>
))}
</PaneNav>
Great, now our active states are being shown in the UI properly!
Try playing around with the initial state and seeing the UI update to reflect different active tabs.
Integrating Machinate: Updating State
To update the state of our app, we need to get access to the manipulator methods (e.g transition
, go
, update
). There are different places where these methods are provided to us in our application. The easiest way to get access to these methods is to use the withMachine
HoC.
First in Pane.js, we'll import the withMachine
HoC. We'll update our import statement to look like so:
import { IsActive, withMachine } from "machinate";
Next, we'll wrap our Pane component where we export it with the HoC.
export default withMachine(Pane);
This gives our Pane component a few additional props to work with. Let's pull in the go
method.
const Pane = ({ go, stretch, navigation, children }) => {
Now we can add an onClick
event handler on our NavItem
, where we pass in go
as a callback with the state to update the machine's state with. When this is invoked, the app will automatically navigate to the given state for the given domain, and de-activate the previous state.
For example, if go("ExternalsView.List")
is invoked, machinate will update the state of "ExternalsView" to become "List". If it was on "Blacklist" before, that state will automatically be deactivated, and your app will update accordingly.
<NavItem
className={active ? "active" : ""}
onClick={go(item.state)}
>
Voila! We now have functioning panels! Sandbox: Functioning panels
This concludes Part 1 of building tooling for Machinate using Machinate.