Code Example

This is a walk-through, please see the tutorials for a complete guide to learning Behavior Graph.

Let’s see how Behavior Graph code looks like by implementing functionality from a typical login screen.

Login Page

Form Validation

As a first feature, we would like the Login button to remain disabled until the user has entered both a reasonable email and password. If the user types in some password but an invalid email address (missing the ‘@’ character, for example) the Login button will remain disabled. Once she corrects the email address by adding an ‘@’ character, the Login button should immediately enable.

Here is Behavior Graph code for this functionality.

import * as bg from "https://cdn.skypack.dev/behavior-graph";

class LoginForm extends bg.Extent {
  constructor(graph) {
    super(graph)

    this.loginEnabled = this.state(false);
    this.email = this.state("");
    this.password = this.state("");
    
    this.behavior()
      .supplies(this.loginEnabled)
      .demands(this.email, this.password)
      .runs(() => {
        const emailValid = validateEmail(this.email.value);
        const passwordValid = this.password.value.length > 0;
        const enabled = emailValid && passwordValid;
        this.loginEnabled.update(enabled);
    });

  }
}

let graph = new bg.Graph();
let loginForm = new LoginForm(graph);
loginForm.addToGraphWithAction();

First we import Behavior Graph to have access to its primary interface objects: Graph and Extent.

Next we subclass Extent which needs to be initialized with an instance of Graph. Extents are containers of Behavior Graph elements. They also provide convenient API for creating these elements.

Resources

Inside the constructor we create 3 resources using the state() factory method: loginEnabled, email, and password. Resources are special containers of information. For example, the loginEnabled resource will say if our Login button should be enabled or not. We initialize it with false because it will be disabled by default.

Validation Behavior

After those we define a behavior. It will determine if the Login Button should be enabled based on the contents of the email and password resources. They appear as parameters to the demands() clause of our behavior() factory method. This list of resources is called the behavior’s demands. They contain the information the behavior depends on.

As stated before, behaviors are never called directly. In specifying a behavior’s demands, we are saying, “whenever any of these resources updates (changes), then this behavior needs to run”. In our example, when either email or password (or both) update, this behavior will run in response.

The block of code specified in the runs() clause is the code that will run. A typical behavior uses normal, imperative code to perform its work. We check the validity of the email with a normal function call. We use normal Boolean logic to determine if the Login button should be enabled.

email and password are a specific type of resource called a state resource. State resources are designed for saving and retrieving information. The contents of a state resource is available via its value property. A behavior must include a resources in its list of demands in order to access its value. Note in the code above how we use .value to access what the Email resource contains in order to validate it.

Supplies

This behavior is responsible for the enabled state of the Login button. We store this information in another state resource called loginEnabled. We list this resource in the supplies() clause of our behavior(). This list is called the behavior’s supplies. A behavior can read and write the contents of these resources. We write to a state resource by calling its update method.

Logging In

We continue to develop our Login page by adding a second feature. When the user clicks the Login button and we are not already logging in, then we would like to enter into a logging in state. In order to prevent mistakes, when we are in a logging in state, we would also like the Login button to be disabled.

To implement this new feature we introduce a second behavior and make a small change to our existing behavior.

this.loginClick = this.moment()
this.loggingIn = this.state(false)

this.behavior()
    .supplies(this.loggingIn)
    .demands(this.loginClick)
    .runs(() => {
        if (this.loginClick.justUpdated && !this.loggingIn.value) {
            this.loggingIn.update(true);
        }
    });

this.behavior()
    .supplies(this.loginEnabled)
    .demands(this.email, this.password, this.loggingIn)
    .runs(() => {
        const emailValid = this.validEmailAddress(this.email.value);
        const passwordValid = this.password.value.length > 0;
        const enabled = emailValid && passwordValid & !this.loggingIn.value;
        this.loginEnabled.update(enabled);
    })

Our new behavior has one demand, loginClick. This is a second type of resource called a moment resource. Moments are designed to track momentary happenings such as a button click or network call returning. We can check if a moment has just happened by accessing its justUpdated property.

When the user clicks on the button, loginClick will update, and this new behavior will run. It performs a simple Boolean check to determine if the loggingIn state resource needs to update to true. It is allowed to update this resource because loggingIn is part of its supplies.

We also modified our previous behavior to include loggingIn as one of its demands. This means it will also run when the loggingIn resource updates. Now the state of loginEnabled depends on all three pieces of information: email, password, and loggingIn.

Actions

Login Behavior Graph

Information comes into our system via actions. A typical UI library will provide some type of callback or event system to capture user inputs. In this example we will listen to a click handler to create a new action which updates the loginClick moment resource.

this.loginButton.onClick = () => {
    this.action(() => {
        this.loginClick.update();
    });
};

We would similarly connect email and password to their respective text fields.

Once the user has entered a valid email and password, the Login button will enable. When the user subsequently clicks on the Login button, the behavior that supplies loggingIn will run. It will update the loggingIn resource to true. This in turn will cause the behavior that supplies loginEnabled to run. It will update the loginEnabled resource to false (because we are logging in).

Side Effects

In order to perform real output to the UI library, we need to create a side effect.

this.behavior()
    .supplies(this.loginEnabled)
    .demands(this.email, this.password, this.loggingIn)
    .runs(() => {
        const emailValid = this.validEmailAddress(this.email.value);
        const passwordValid = this.password.value.length > 0;
        const enabled = emailValid && passwordValid & !this.loggingIn.value;
        this.loginEnabled.update(enabled);

        this.sideEffect(() => {
            this.loginButton.enabled = this.loginEnabled.value;
        });
    })

Side effects are created directly inside behaviors. Inside are are allowed to do anything we like. This side effect updates the enabled state of the loginButton based on the state of the loginEnabled resource. It does not run immediately, however. Behavior Graph defers the running of side effects until after all behaviors have run. Side effects are a practical way for Behavior Graph to create output while ensuring access to consistent state.

Learning More

Congratulations, you have just learned all the important concepts for using Behavior Graph in your code.

Please don’t be discouraged by the number of new abstractions. This walk-through is intentionally brief. But from here on out, it’s mostly just nuance.

To learn more, please take a look at our tutorials.