Skip to content
Wyatt Allen edited this page Apr 14, 2014 · 14 revisions

Getting Started

First, make sure you have git and npm installed. You can download the repository by issuing the following commands.

git clone git@github.com:whatgoodisaroad/cablejs.git
cd cablejs

Next, setup the dependencies for examples with these commands. The examples depend on client-side libraries such as jQuery and d3.js, and bower will prepare all of them so that the examples can work.

npm install
cd examples
bower install

Now that cable is downloaded an ready, we can create an HTML document that loads the cable script.

<html>
  <head>
    <title>Cable</title>
  </head>
  <body>
    <script src="path/to/cable.min.js"></script>
    <script>
      Cable.define({
        //  Node definitions go here!
      });
    </script>
  </body>
</html>

As you can see, this document loads up cable followed by a script which executes a function named Cable.define with an empty object as argument. What Cable.define does is allow us to create nodes in the cable "graph", in other words, the functionality of our app will be built up just by adding properties in that empty object.

Basic Input and Output

Let's make a really simple app that takes input in a textbox, and presents a message based on that input. First, we add <input> and <span> elements to the body.

<input id="input" type="text" value="" />
<span id="output"></span>

Now, we need to create a node in the cable graph to represent the input. This node will represent the stream of data corresponding to the value in the textbox. In cable, this is technically an event node, because it represents the event of there being a new value in the textbox.

We can create an event node by writing a property in the definition object as a function which accepts one argument named event. What an event function must do is configure the DOM or some event system such that it invokes the event argument with the new value. So, for a textbox, this means binding to the onchange and onkeyup events with a function that invokes the event argument with the value property of the textbox.

input:function(event) {
  var 
    e = document.getElementById("input"),
    f = function() { event(e.value); };

  event.setDefault(e.value);

  e.onchange = f;
  e.onkeyup = f;
}

We have now incorporated the textbox into the cable graph as an input. But input isn't useful at all without a way to do output. Let's set it up so that it outputs the current value of the textbox in the <span>.

In cable, this will be an effect node. We can write it by making a function which accepts the name of some other node as argument to be used in producing the effect. In this case, we want to make use of the node named input to display its value, so we'll write a function with an argument named input.

Cable sets up the input argument to be a getter for the input value, so we can produce the output string by invoking it. This informs the cable system to execute this function every time the value. See below.

output:function(input) {
  document.getElementById("output").innerText = (
    "You typed '" + input() + "'"
  );
}

Putting it all together we have the following HTML file.

<html>
  <head>
    <title>Cable</title>
  </head>
  <body>
    <input id="input" type="text" value="" />
    <span id="output"></span>
    <script src="path/to/cable.min.js"></script>
    <script>
      Cable.define({
        input:function(event) {
          var 
            e = document.getElementById("input"),
            f = function() { event(e.value); };
          event.setDefault(e.value);
          e.onchange = f;
          e.onkeyup = f;
        },
        output:function(input) {
          document.getElementById("output").innerText = (
            "You typed '" + input() + "'"
          );
        }
      });
    </script>
  </body>
</html>

Now, if I load this page and type "oranges" into the input, the span says:

You typed 'oranges'

This file is a working cable app. It will display whatever you type in the textbox and update the span as the input changes. However, it's a bit boring to display the value in the textbox without changing it. Let's do something more creative by creating a reversed version of the string.

First, think about reversing the string. The reversed version of the string isn't an event and it also isn't really an effect. Instead, it's something that exists between input and output. which, in cable is called a synthetic node because it synthesizes other values into a new one.

In this case, we'll be synthesizing the input into the reversed input. This is done by creating a function that accepts an argument named input (so we can use the value of the textbox), but also accepts an argument named result. We can pass a value into result to represent the synthesized data.

reversed:function(input, result) {
  result(input().split("").reverse().join(""));
}

Reversed now acts like a stream of data representing the reversed version of the stream of input data.

Now that this is defined, we can display this additional information by rewriting the output node to use reversed in a similar way to how input is used.

output:function(input, reversed) {
  document.getElementById("output").innerText = (
    "You typed '" + input() + "' the reversed is '" + reversed() + "'"
  );
}

Now, if I type "apples" into the input, the span says:

You typed 'apples' the reversed is 'sellpa'

With these streams, we can define an even more abstract synthetic which detects a whether the input is a palindrome.

isPalindrome:function(input, reversed, result) {
  result(input() === reversed());
}

And incorporate this into the result.

output:function(input, isPalindrome) {
  document.getElementById("output").innerText = (
    "'" + input() + "' is " + 
    (isPalindrome() ? "" : "not ") +
    "a palindrome"
  );
}

It's useful that isPalindrome is somewhat abstract. It's a stream of data which represents a relationship between two other streams of data. This is a powerful tool because you can write a synthetic node to represent all sorts of abstract aspects of your application, and nodes which use such nodes are updated automatically. These qualities could be things like isUserLoggedIn, isGameOver, or canZoomInFurther.

Simpler Input and Output with jQuery

Declaring the input and output as we've done in the previous section is conceptually simple, but syntactically verbose. If we add jQuery to our application, cable provides some helpers which make declaring them more convenient.

First, let's add jQuery to cable by declaring it as a library, then, declare the input and output with cable helper functions. These new versions of input and output work in the same way as before.

Cable.define({
  $:Cable.library("path/to/jquery.js"),

  input:Cable.textbox("#input"),
  output:Cable.template(
    "#output", 
    "You typed '{{input}}' and the reversed is {{reversed}}"
  )
});

Building Game

Now that we have a grasp on how to define cable nodes, let's build a more complex application, specifically a clone of the popular JavaScript game 2048. The final result of this tutorial can be found in the examples/2048 directory of the repository.

For the actual rules of the game, we'll use am implementation named 2048.js. This file has nothing to do with cable specifically, it's just a stand-alone, pure-functional implementation in normal JavaScript. The role of cable is a glue-layer to connect this implementation to the DOM.

2048.js provides the following functions:

  • blank(): Create a blank game state with no numbers.
  • initialize(): Create an initial state for the game (i.e. a blank state plus two numbers spawned).
  • move(state, direction): Accept a state and a direction (i.e. "north", "south", "east" or "west") and produce a new state created by applying that rule.
  • isGameOver(state): Accept a state and return a boolean of whether the game is over (i.e. none of the directions are possible).
  • isGameWon(state): Accept a state and return a boolean of whether the game has been won (i.e. at least one tile is the winning tile -- 2048).

The format of the state values has these properties:

  • score: The current score
  • step: The number of the most recently applied move.
  • grid: A 2-D array of integers representing thr game-board. 0 is used for blank spaces.
  • last.created:` A list of tiles that were created in the last turn.
  • last.moved:` A list of tiles that shifted in the last turn.
  • last.created:` A list of tiles that were merged into larger ones in the last turn.

We want to connect this set of game functions to a webpage. Let's make an HTML document and load up cable just like before. Notice, we've made some HTML elements for the game visuals, namely a <div> with id grid to display the board, and a <div> with id controls to house a reset button and display the score.

<html>
  <head>
    <title>2048 Cable</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>

    <div id="main">
      <div id="grid"></div>  
      <div id="controls">
        <input id="reset" type="button" value="Reset" />
        <span id="score"></span>
      </div>
    </div>

    <script src="../../dist/cable.min.js"></script>
    <script>
      Cable.define({
        
      });
    </script>

  </body>
</html>

Now that that is set up, the remainder of the work will go in that Cable.define argument. All we have to do is create properties in the cable definition object which describe how to connect the game to the document. The next step will be to bring in some libraries, namely the 2048.js library and jQuery.

Cable.define({
  $:Cable.library("jquery.min.js"),
  game:Cable.library("2048.js")
});

Next, we want some inputs, let's make an event for when the direction keys are pressed, and another event for when the reset button is pressed. The restart event uses a helper function built into cable for making events based on buttons. In the case of the direction, we define the event more manually by creating an event listener for the onkeyup event of the document.

Cable.define({
  direction:function($, define) {
    define(function(event) {
      $(document).on("keyup", function(e) {
        switch (e.which) {
          case 37: event("west");   break;
          case 38: event("north");  break;
          case 39: event("east");   break;
          case 40: event("south");  break;
        }
      });
    });
  },
  restart:Cable.button("#reset")
});

We now have our libraries, and we have the inputs to the system, this is all we need to connect the game mechanics together. In this set of definitions, we declare a data node named state to house the game state. Data nodes like this simply hold some JavaScript value, and update its dependent ndoes whenever it gets changed.

Cable.define({
  state:Cable.data(null),

  setup:function(init, restart, game, _state) {
    _state(game.initialize());
  },

  move:function(game, direction, _state) {
    if (!game.isGameOver(_state()) && !game.isGameWon(_state())) {
      _state(game.move(_state(), direction()));
    }
  }
});

We create two effects named setup and move to setup a new game and to apply movement commands respectively. setup listens to two events: the restart event we created before, and init. init is a special event which fires when cable is loaded, so, this forces the system to setup a new game when the page loads. It creates a new state using the game.initialize() function, and sets the state node with this value. Notice that the _state argument has an underscore in front of it. Putting an underscore before an argument name like this prevents the function from being updated when state changes. In this case we don't want this function to be updated because it is the function that is making the change.

The move effect listens to the direction inputs and updates the state by applying the direction command with the game.move(s, d) function. As before with setup, we prefix the _state argument name with an underscore because this effect is making the change to the data node.

With this mechanism in place, we can put together some useful synthetic nodes which map out interesting properties of the state.

Cable.define({
  gameOver:function(game, state, result) {
    result(game.isGameOver(state()));
  },

  won:function(game, state, result) {
    result(game.isGameWon(state()));
  },

  score:function(state, result) {
    result("" + state().score);
  }
});

gameOver and won are nodes which represent streams of booleans for whether the game is over (i.e. failure) or whether it has been won (i.e. the 2048 tile has been created). score is a stream of the game's score, converted into a string.

Now the mechanism is in place, but one important thing remains, we need to display the game to the player. For organization, we will wrap all of the code for displaying the game in a scope named render. In this scope, we'll start with the simplest thing to display, the game's score, which we can do with a template definition.

Cable.define({
  render:{
    showScore:Cable.template("#score", "Score: {{score}}")
  }
});

Let's use the Jade templating engine for rendering the grid as an HTML <table>. Write the following template in a file named grid.jade.

table
  each row, ri in grid
    tr
      each col, ci in grid[ri]
        td.cell(data-value = grid[ri][ci])
          span

Now, to use the template, we can load up the jade rendering libaray, and the text of this template file, with a couple of resource nodes named jade and template. Finally, putting these together, along with jQuery and the stream of game state values, write an effect named grid which renders the table with the current state, and puts the result inside of the <div> named grid. Because grid node listens to the state node, this render happens immediately, every time that the state changes.

Cable.define({
  render:{
    jade:Cable.library("jade.js"),
    template:Cable.text("grid.jade"),

    grid:function($, jade, template, state) {
      $("#grid").html(jade.render(template(), state()));
    }
  }
});

There is only one last detail: we want to render differently when the game is won or lost. This is accomplished with a straightforward use of jQuery.

Cable.define({
  render:{
    over:function($, gameOver, won) {
      if (gameOver() || won()) {
        $(".cell").animate({ opacity:0.5 });
        alert(gameOver() ? "Game over" : "You win!");
      }
      else {
        $(".cell").animate({ opacity:1 });
      }
    }
  }
});

Putting it all together, we have the following file. In the repository exaples, this corresponds to examples/2048/v1.html.

<html>
  <head>
    <title>2048 Cable</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>

    <div id="main">
      <div id="grid"></div>    
      <div id="controls">
        <input id="reset" type="button" value="Reset" />
        <span id="score"></span>
      </div>
    </div>

    <script src="cable.min.js"></script>
    <script>
      Cable.define({
        $:Cable.library("jquery.min.js"),
        game:Cable.library("2048.js"),

        direction:function($, define) {
          define(function(event) {
            $(document).on("keyup", function(e) {
              switch (e.which) {
                case 37: event("west");   break;
                case 38: event("north");  break;
                case 39: event("east");   break;
                case 40: event("south");  break;
              }
            });
          });
        },
        restart:Cable.button("#reset"),

        state:Cable.data(null),
        setup:function(init, game, _state, restart) {
          _state(game.initialize());
        },
        move:function(game, _state, direction) {
          if (!game.isGameOver(_state()) && !game.isGameWon(_state())) {
            _state(game.move(_state(), direction()));
          }
        },

        gameOver:function(game, state, result) {
          result(game.isGameOver(state()));
        },
        won:function(game, state, result) {
          result(game.isGameWon(state()));
        },
        score:function(state, result) {
          result("" + state().score);
        },

        render:{
          jade:Cable.library("jade.js"),
          template:Cable.text("grid.jade"),

          grid:function($, jade, template, state) {
            $("#grid").html(jade.render(template(), state()));
          },

          over:function($, gameOver, won) {
            if (gameOver() || won()) {
              $(".cell").animate({ opacity:0.5 });
              alert(gameOver() ? "Game over" : "You win!");
            }
            else {
              $(".cell").animate({ opacity:1 });
            }
          },

          showScore:Cable.template("#score", "Score: {{score}}")
        }

      });
    </script>
  </body>
</html>

Viola! The game, 2048.