This example shows an implementation of the child's game of tic tac toe. The implementation requires a component and back-end class for the overall game plus for each the boxes (9 copies of the same thing). Bootstrap's grid classes are used to make the game board, which means the UI doesn't need to care about what's on what row.
Usings: Three Ichigo components are used in this page. Rather than reference them throughout by the full namespaced name (mi5.blah.blah.blah), this uses the mi5.using function to assign the variable in the first argument to the alias in the second argument. After running mi5.using (mi5.component.BoundComponent, 'BoundComponent')
, you can reference it simply by 'BoundComponent' (or whatever).
GameModel: This sets up properties to store game state and methods to implement the rules of the game.
The GameModel constructor sets up the object. Only variables that need to be updated in the UI are using ObservableProperties, and there are only four of them, total. The React demo I'm ripping off used state objects, but this really too simple for that. State is important to React, so it's worth demoing it even if it's overkill, but Ichigo doesn't care. Ichigo is all about simplicity.
- playerX: Indicates if X is playing (else it's O)
- status: A text message that is displayed
- won (not observable): Tracks internally if the game has been won or not
- moves (not observable): Tracks internally if the game is unwinnable (all 9 moves made)
The boxes themselves are stored as a 3x3 map of BoxModel, where the map key is x + comma + y. There are 9 possible winning layouts. We brute force it by storing all in an array.
The set()
method, which is called when a box is clicked, halts if the game is already won. It looks up the BoxModel by using Map's get() method and if value
is not blank, halts because the box has already been played (setting the status displayed to INVALID MOVE). Otherwise, it sets value
and then checks to see if the play is a winning play by calling checkWin().
The checkWin()
method is called for whoever just clicked a box and goes through each of the 8 winning moves and sees if every one of the boxes matches that value. If so, the user has won and the status displayed on the screen becomes GAME WON. Also, it sets winningMove.value
on the squares that were responsible for the win to true. If the user did not move, then moves
is incremented and the displayed status is updated.
The currentPlayer()
method returns X or O depending on the value of the playerX boolean.
The clear()
method resets everything to start.
BoxModel: BoxModel creates two observable properties for each box and creates a getter and setter to quickly access the value of xy.
- xy: Stores either X or Y, for whoever played this box
- winningMove: After the game is won, this stores true if the box was part of the winning move
GameView: GameView creates an instance of GameModel and generates the overall HTML for the game board:
super(new GameModel(), { outerHtml: ` [ A BUNCH OF HTML ] `, observeAllViewModel: true });
The HTML uses the following Ichigo features:
<div class="row" :loop="db"><div class="col-4"></div></div>
: Creates a loop for every item in "db" whose items are the top-level div. "row" and "col-4" are part of the Bootstrap grid system. See below for notes about this child loop.
Current Player: <i-v>currentPlayer</i-v>
: Displays the value of currentPlayer, which is a method in this case, showing the value of the playerX observable property.
Status: <i-v>status</i-v>
: Displays the value of status, which is an observable property.
<button class="btn btn-secondary w-100" type="button" :event (click)="reset" component="reset">Reset</button>
: When the addInlineListeners()
event is called, the reset method is bound to the reset button's click event.
GameView's child loop: Custom attributes are more complicated than i-v tags, which need only a string. They work by modifying content
(which is a property of the BoundComponent) and getting data from viewModel
(likewise) and use a number of other properties. Because of this, only the topmost element is processed for custom attributes, such as :loop
.
In this example, you'll see the loop is a child of GameView, so the :loop
attribute isn't part of GameView. But the loop is so simple that it would be a waste to create a LoopComponent class; it can work with nothing more than default functionality. So we write an inject statement to inject a simple BoundComponent. BoundComponent.injectBind(this.viewModel, '.row', { parent: this.content, loopItemClass: BoxView })
. This finds the row and makes it a component whose loop items are BoxView.
(There is an auto-inject method, but we cannot use it for the loop, because the loop requires additional properties (loopItemClass), which cannot be predicted automatically. We must write it manually. I added an auto-injection method but it's kind of useless without user-specified options. I may be deleting it or replacing it with a function that just returns all elements with Ichigo custom properties.)
BoxView: This class is called once for each row in GameView's loop, after the looping HTML has been added to the page. This class turns that HTML into a component.
In this case, because it is more attractive to have a box with wide borders between columns, and the Bootstrap grid system fails if there are margins between columns, it puts an inner div inside the outer div, with a nice bit of margin in the CSS: <div class="box" :event (click)="onClick" component="box"><i-v>value</i-v></div>
The viewModel passed into the BoxView is a BoxModel, which has properties for value
(displayed in the div) and winningMove
. The winningMove property is used in a custom attribute, which you'll remember from the GameView loop discussion above needs to be stored at the component level. options.attributes = { ':switch:success': 'winningMove' }
adds :switch:success
to the <div class="col-4">
element (there are other ways to skin this cat but that's the one I liked).
Once the options have been prepared, the BoundComponent constructor is called using super(); The x/y coordinates for the box are stored, along with a reference to the GameModel.
const [x, y] = loopItem[0].split(',');
this.x = x;
this.y = y;
this.game = options.parent.viewModel;
The onClick() event calls the game's set()
method.
You'll notice that no logic happens in GameView, BoxView, or even BoxModel. Everything happens in GameModel, which triggers updates to the UI whenever an ObservableProperty is updated.