Cookbook: MVC

MVC allows for a clean an testable separation between the behavior (controller) and the view (HTML template). A Controller is just a JavaScript class which is grafted onto the scope of the view. This makes it very easy for the controller and the view to share the model.

The model is simply the controller's this. This makes it very easy to test the controller in isolation since one can simply instantiate the controller and test without a view, because there is no connection between the controller and the view.

    <script>
    function TicTacToeCntl($location){
      this.$location = $location;
      this.cellStyle= {
        'height': '20px',
        'width': '20px',
        'border': '1px solid black',
        'text-align': 'center',
        'vertical-align': 'middle',
        'cursor': 'pointer'
      };
      this.reset();
      this.$watch('$location.search().board', this.readUrl);
    }
    TicTacToeCntl.prototype = {
      dropPiece: function(row, col) {
        if (!this.winner && !this.board[row][col]) {
          this.board[row][col] = this.nextMove;
          this.nextMove = this.nextMove == 'X' ? 'O' : 'X';
          this.setUrl();
        }
      },
      reset: function() {
        this.board = [
          ['', '', ''],
          ['', '', ''],
          ['', '', '']
        ];
        this.nextMove = 'X';
        this.winner = '';
        this.setUrl();
      },
      grade: function() {
        var b = this.board;
        this.winner =
          row(0) || row(1) || row(2) ||
          col(0) || col(1) || col(2) ||
          diagonal(-1) || diagonal(1);
        function row(row) { return same(b[row][0], b[row][1], b[row][2]);}
        function col(col) { return same(b[0][col], b[1][col], b[2][col]);}
        function diagonal(i) { return same(b[0][1-i], b[1][1], b[2][1+i]);}
        function same(a, b, c) { return (a==b && b==c) ? a : '';};
      },
      setUrl: function() {
        var rows = [];
        angular.forEach(this.board, function(row){
          rows.push(row.join(','));
        });
        this.$location.search({board: rows.join(';') + '/' + this.nextMove});
      },
      readUrl: function(scope, value) {
        if (value) {
          value = value.split('/');
          this.nextMove = value[1];
          angular.forEach(value[0].split(';'), function(row, col){
            this.board[col] = row.split(',');
          }, this);
          this.grade();
        }
      }
    };
    </script>

    <h3>Tic-Tac-Toe</h3>
    <div ng:controller="TicTacToeCntl">
    Next Player: {{nextMove}}
    <div class="winner" ng:show="winner">Player {{winner}} has won!</div>
      <table class="board">
        <tr ng:repeat="row in board" style="height:15px;">
          <td ng:repeat="cell in row" ng:style="cellStyle"
              ng:click="dropPiece($parent.$index, $index)">{{cell}}</td>
        </tr>
      </table>
      <button ng:click="reset()">reset board</button>
    </div>
  
    it('should play a game', function() {
     piece(1, 1);
     expect(binding('nextMove')).toEqual('O');
     piece(3, 1);
     expect(binding('nextMove')).toEqual('X');
     piece(1, 2);
     piece(3, 2);
     piece(1, 3);
     expect(element('.winner').text()).toEqual('Player X has won!');
    });

    function piece(row, col) {
      element('.board tr:nth-child('+row+') td:nth-child('+col+')').click();
    }
  

Things to notice