JavaScript "Masterclass"
Craig Spence π¦
JS @ Trade Me
@phenomnomnominal οΈ
What we're going to cover
JavaScript in 2016
βοΈ Language
βοΈ Tooling
βοΈ Community
Writing a server in JavaScript
Writing client-side web applications in JavaScript
JavaScript in 2016
The language has evolved
ES2015 (& ES2016) - ratified and being implemented in evergreen browsers
Fragmented (but converging) eco-system
Super popular - and in demand!
POWERFUL AF π― π₯ π
JavaScript
var Pizza = (function () {
var Pizza = function Pizza (options) {
this.flavour = options.flavour;
this.size = options.size;
};
return Pizza;
})();
var Human = (function () {
var Human = function Human (options) {
this.name = options.name;
};
Human.prototype.eat = function (food) {
var flavour = food.flavour
var foodType = food.constructor.name;
alert('Mmmm, ' + flavour + ' ' + foodType);
};
return Human;
})();
var pizza = new Pizza({
flavour: 'margherita'
});
var craig = new Human({
name: 'Craig'
});
craig.eat(pizza);
class Pizza {
constructor (options = {}) {
Object.assign(this, options);
}
}
class Human {
constructor (options = {}) {
Object.assign(this, options);
}
eat (food) {
let { flavour } = food;
alert(`Mmmm, ${flavour} π`);
}
}
let pizza = new Pizza({
flavour: 'margherita'
});
let craig = new Human({
name: 'Craig'
});
craig.eat(pizza);
ES5
ES2016
#foreshadowing π
JavaScript
ECMA TC-39 work on the ECMAScript specification
New versions to be released annually! π―π―π―
ES2015 was huge - classes, promises, template strings, destructuring, meta-programming, modules & more!Β
ES2016 - not so huge:
// Array.prototype.includes - check if an array contains a given object
[1, 2, 3, 4].includes(1); // true
['a', 'b', 'c'].includes('d'); // false
// Exponentiation operator:
console.log(2 ** 3); // 8 - Equivalent to Math.pow(2, 3); or 2 * 2 * 2;
Tooling
Tooling
npm ecosystem is HUGE π
> 300,000 modules π
> 1,000,000,000 downloads a week π±
Knowing what modules to use can be a nightmare βοΈ
(but that's a whole other topic)
YAY code re-use
BOO dependencies
#notmyleftpad
Community
SUPER ACTIVE! - npm, Github, etc.
Local meetups:
JavaScript NZ
#letswritecode
We're going to write a real-time multi-player game!
Start by going to
https://github.com/phenomnomnominal/summer-of-tech-js-masterclass
Click the "Clone or download" button, then "Download ZIP"
Unzip the project to your computer somewhere, and then open a terminal and navigate to the unzipped project folder.
INSTALLING A DEPENDENCY
It is also where the listing of the projects dependencies will live.
To see what I mean by that, let's add our first dependency to the project. Copy the following command and paste it into a terminal:
npm install --save express
OUR FIRST DEPENDENCY!
Have a look in the package.json file again. See how there's a new entry under dependencies? That line means that when someone else installs this project, it know that it needs Express to run.
Β
You'll also notice that a node_modules folder has appeared! That contains all the code needed for Express.
So, what exactly have we installed?
Express is a very commonly used npm module. It is a "Fast, unopinionated, minimalist web framework". We're going to use this to write the back-end for our game!
STARTING A SERVER
We now need somewhere to write some code. Let's create an index.js file in the root of our project folder, next to package.json.
Within that file, we're going to add three bits of code:
import express from 'express';
let app = express();
app.get('/', (req, res) => {
res.send('Hello World!');
});
const server = app.listen(3000, () => {
console.log(`Server is running on port ${server.address().port}!`);
});
1β£οΈ
2β£οΈ
3β£οΈ
STARTING A SERVER
There's a few things to notice here:
import express from 'express';
We've used the ES2015 module import syntax:
We've created some objects:
let app = ...
let server = ...
We've registered a few callbacks:
app.get('/', (req, res) => { ... });
app.listen(3000, () => { ... });
Don't Call me, I'll Call You
The idea of a callback is a very important concept in JavaScript - both on the client and the server.
JavaScript has a conceptually simple execution model. Code runs in a single thread, one instruction after another, until it runs out of things to do.Β
When a typical JavaScript application starts, event listeners are created. On a web page, these events might be things like a mouse click or keypress. On a web server, these events are typically HTTP requests.
app.get('/', (req, res) => { ... });
This says, when the application receives a GET request, run this function. That function is the callback.Β
RUNNING THE SERVER
First, let's try running the server!
node index.js
From your terminal, run the following command:
RUNNING THE SERVER
π£π₯π£π₯π£π₯
Did everything break? Good π
That's expected, because we are using JavaScript from the future!
While ES2015/2016 have been standardised, they haven't yet been
implemented in all the different JavaScript run-times.
FUTURISING OUR CODE
We need to add a few more dependencies:
npm install --save babel-cli babel-preset-es2015
This time, we've added Babel, and a Babel preset.
Babel is a tool for transforming JavaScript code. The ES2015 preset knows how to turn modern JavaScript into the equivalent code, but only using older, ubiquitous language features.
Now we just need to tell Node to use Babel when it runs the code. We will do that with an npm script.
npm Scripts
There's a lot of tools out there for running build tasks on projects.
The main ones are Gulp, Grunt, Broccoli and Fly - all are roughly equivalent, just slightly different implementations of the same idea.
We're going to go a slightly simpler, but equally valid route - especially for a smaller project like this one. We're going to add an npm script. Open up the package.json file again, and replace the scripts block with the following:
"scripts": {
"start": "babel-node --presets es2015 index.js"
},
npm Scripts
This means that when the "start" script is executed, it will use babel-node with the es2015Β preset to run the index.js file.
npm run start
We run that script by running the following from a terminal:
We should now see the following!
Server is running on port 3000!
SUCCESS! πππ
Go to http://localhost:3000 in a browser to see it working!
OUR GAME
We're going to make two player tic-tac-toe!
We need three endpoints for it to work:
GET game-state
POST join-game
POST take-turn
STUB END POINTS
Let's add the outline of our endpoints...
app.post('/join-game', (req, res) => {
// ...
});
app.get('/game-state', (req, res) => {
// ...
});
app.post('/take-turn', (req, res) => {
// ...
});
Add the following to our index.js file
β οΈ make sure you add these *after* the line that creates the app
REPRESENTING GAME STATE
The game state endpoint is going to return all the information about what is going on in the game. Β That information will look something like this:
{
"players": [{
"name": "Player One",
"symbol": "X"
}, {
"name": "Player Two",
"symbol": "O"
}],
"moves": [0, 1, 2, 3, 4, 5, 6, 7, 8],
"whoseTurn": null,
"winner": null
}
The "players" array contains information about the players
The "move" array is the list of moves that have been made, where "0" is the top-left box, and "8" is the bottom-right.
The "whoseTurn" or "winner" fields will be updated as each move is made.
GAME STATE
Let's add some code to represent this on the server. We'll make a new file called game-state.js, and add the following to it:
export default class GameState {
constructor () {
this.players = [];
this.moves = [];
this.whosTurn = null;
this.winner = null;
}
addPlayer (player) {
if (this.players.length < 2) {
this.players.push(player);
}
}
addMove (turn) {
let { move } = turn;
if (this.players.length < 9) {
this.moves.push(move);
}
}
}
CREATING A NEW GAME STATE
Now let's use our new GameState class.
In index.js, we need to import the class, after where we imported express:
import express from 'express';
import GameState from './game-state';
Notice that we've used a path (starting with ./) rather than just a module name. This tells node that we want to import a local file rather than something from node_modules.
We're going to create a new GameState when the application starts as well:
let app = express();
let gameState = new GameState();
GETTING THE GAME STATE
And finally, let's update our GET game-state endpoint to return the game state as JSON:
app.get('/game-state', (req, res) => {
res.json(gameState);
});
This is the power of Express doing a bunch of work for us! It takes the object that represents the GameState and turns it into JSON and sends it back as the response.
npm run start
If we restart our application by running the following again:
We can then check that our game state endpoint works by running the following in a second terminal:
curl http://localhost:3000/game-state
UPDATING THE GAME STATE
So now that we can GET the game state, we need to be able to POST to our server to update it!
We want to be able to send JSON data to the server and use that to update our gameState object. To do that, we need to add another dependency:
npm install --save body-parser
First we need to import our dependency so we can use 'body-parser'. Add the import to the top of the index.js file:
import bodyParser from 'body-parser';
UPDATING THE GAME STATE
Then, we need to tell the app to parse request bodies as JSON:
app.use(bodyParser.json());
β οΈ make sure you add these *after* the line that creates the app
UPDATING THE GAME STATE
Now we can update our POST endpoints to update the gameState object:
app.post('/join-game', (req, res) => {
console.log(req.body);
let { name, symbol } = req.body;
gameState.addPlayer({ name, symbol });
res.status(200).end();
});
app.post('/take-turn', (req, res) => {
console.log(req.body);
let { move } = req.body;
gameState.addMove({ move });
res.status(200).end();
});
Here we are doing pretty much the same thing both times - get some data off the request body, update the game state, and return a "200" response. We've also added some logging so we can see what is happening.
TESTING OUR POST METHODS
npm run start
If we restart our application by running the following again:
We can then check that our two new endpoints work by running
curl -H "Content-Type: application/json" -X POST -d '{"name":"Craig","symbol":"X"}' http://localhost:3000/join-game
and
curl -H "Content-Type: application/json" -X POST -d '{"move":2}' http://localhost:3000/take-turn
β οΈ you'll need to run these in a second terminal - the node app has to be running at the same time to respond to the requests!
TESTING OUR POST METHODS
If that worked, we should see some logging in the terminal that has our node server running. And if we do another GET on our game-state endpoint, we should see that it has been updated!
curl http://localhost:3000/game-state
Input Validation
Currently we have no validation on our endpoints. We need to make sure we validate on the server, so that any bad data that comes in a request doesn't cause any issues. Let's add the following to the start of the addPlayer method of the GameState class:
if (!player.name) {
throw new Error('Invalid player: no name');
}
if (!player.symbol) {
throw new Error('Invalid player: no symbol');
}
if (('' + player.symbol).length !== 1) {
throw new Error('Invalid player: symbol should be a single character');
}
Let's restart our server and fire off an invalid request to the POST join-game endpoint:Β
curl -H "Content-Type: application/json" -X POST -d '{}' http://localhost:3000/join-game
Input Validation
π£π₯π£π₯π£π₯
Everything broke again! But that's okay, it broke because we told it to! Let's update our endpoint code to handle the error more gracefully. Back in index.js:
app.post('/join-game', (req, res) => {
let { name, symbol } = req.body;
try {
gameState.addPlayer({ name, symbol });
res.status(200).end();
} catch (e) {
let { message } = e;
res.status(400).json({ message });
}
});
Now our server will respond with the correct error code, and an error message.
Input Validation
Let's do the same thing for the POST take-turn endpoint too:
app.post('/take-turn', (req, res) => {
let { move } = req.body;
try {
gameState.addMove({ move });
res.status(200).end();
} catch (e) {
let { message } = e;
res.status(400).json({ message });
}
});
And we will change the addMove method of the GameState class to throw an error too. Add the following to the start of addMove, after the first line:
if (isNaN(+move) || move < 0 || move > 8) {
throw new Error('Invalid turn: move should be a number from 0 to 8');
}
BUsiness rules
We're going to use the same mechanism to fail safely when the users do valid things that are against the rules of the game. Currently, you can keep adding as many people to the game as you want π©βπ©βπ§βπ¦ π©βπ©βπ¦βπ¦ π©βπ©βπ§βπ§ ! But tic-tac-toe is only a two player game!
if (this.players.length === 2) {
throw new Error('This game is already full');
}
Now, because of the code we added before, we will get a sensible error message if too many people try to join a game π
Let's change the addPlayer function to fix that. Add this to the top of the function, after the existing validation:
BUsiness rules
We also want to make sure that you can't make a move that the other player has already made. Let's add the following to the top of the addMoves method, but after the other validation code:Β
if (this.moves.indexOf(move) > -1) {
throw new Error('Invalid move: that space is already taken');
}
Again, now we'll get a nice error message!
Taking Turns
Now we can get our server to manage whose turn it is!
if (this.players.length === 2) {
this.whoseTurn = this.players[Math.floor(Math.random() * 2)];
}
Add the following to the end of our addPlayer method to set the initial state of the game:
That will randomly give the first move to one of the two players.
Then we should add the following to the end of the addMove method:
this.whoseTurn = this.players.find(player => player !== this.whoseTurn);
Taking Turns
We need some sort of protection against someone taking two turns in a row - or a third-party making a request to make a turn!
We're going to require that a token is sent with each request to identify the client - and we will only give out that token when a player joins the game successfully. Let's start with that bit first.
Back in game-state.js we're going to import Node's build in crypto module:
import crypto from 'crypto';
We're also going to create an array to store the tokens in (we'll see why in a sec):
const TOKENS = [];
Taking Turns
Now we're going to create a new token whenever a player joins a game successfully. Add this at the end of the addPlayer function:
let token = crypto.randomBytes(64).toString('hex');
TOKENS.push(token);
return token;
Then, we're going to update our POST join-game endpoint to return that token:
let token = gameState.addPlayer({ name, symbol });
res.status(200).json({ token });
And we need to update our POST take-turn endpoint to require that token:
let { move, token } = req.body;
try {
gameState.addMove({ move, token });
res.status(200).end();
} catch (e) {
// ...
Taking Turns
And finally, add one more bit of validation to make sure we got the right token for the player whose turn it is. Add the following to the top of the addMove function:
let { token } = turn;
if (!token || TOKENS[this.players.indexOf(this.whoseTurn)] !== token) {
throw new Error('Invalid turn: it is not your turn');
}
NICE! πͺ
Determining The Winner!
Making moves is all well and good, but we really want to be able to see who won!
Let's make a new file called win-checker.js. It's basic outline should look something like this:
export default class WinChecker {
checkWin (gameState) {
return false;
}
}
For now it just returns false, but we will fix that soon!
Determining The Winner!
Let's go back to game-state.js and import our new class:
import WinChecker from './win-checker';
let winChecker = new WinChecker();
And then update the addMove method to use it:
let win = winChecker.checkWin(this);
if (win) {
this.winner = this.whoseTurn;
this.whoseTurn = null;
} else {
this.whoseTurn = this.players.find(player => player !== this.whoseTurn);
}
Determining The Winner!
Now let's go back to win-checker.js and make it actually work!
First we need a representation of the different moves that make a win:
const WIN_SCENARIOS = [
[0, 3, 6], // First column
[1, 4, 7], // Second column
[2, 5, 8], // Third column
[0, 1, 2], // First row
[3, 4, 5], // Second row
[6, 7, 8], // Third row
[0, 4, 8], // Left-top to right-bottom diagonal
[2, 4, 6] // Right-top to left-bottom diagonal
];
Then we need Β a way to check if a set of moves matches one of the win scenarios. Let's add this to the WinChecker class:
checkWinScenario (winMoves, currentPlayerMoves) {
return winMoves.every((move) => currentPlayerMoves.indexOf(move) !== -1);
}
This just checks that every one of the moves in a particular win scenario is in the list of the players moves.
Determining The Winner!
Now we need to run the checkWinScenario method against each of the possible scenarios. Let's add another function to the WinChecker class:
checkPlayerWin (currentPlayerMoves) {
return WIN_SCENARIOS.some((winMoves) => this.checkWinScenario(winMoves, currentPlayerMoves));
}
This method goes through each of the possible win scenarios checks if any of them match the current players move. Only one has to match for it to be successful.
All we need now is a way to determine what moves are the current players moves!
Determining The Winner!
We're going to replace the stubbed function we had before with the real implementation:
checkWin (gameState) {
let reversedMoves = gameState.moves.slice(0).reverse();
let currentPlayersMoves = reversedMoves.filter((_, i) => !(i % 2));
return this.checkPlayerWin(currentPlayersMoves);
}
We're being a bit tricky here, so let's break it down:
We know the last move that happened was made by the current player, as was every second move before that.
So, we reverse the array, and then filter out every odd numbered move (the other players moves). That leaves only the current players moves!
Determining The Winner!
Our finalised WinChecker class should look like this:
const WIN_SCENARIOS = [
[0, 3, 6], // First column
[1, 4, 7], // Second column
[2, 5, 8], // Third column
[0, 1, 2], // First row
[3, 4, 5], // Second row
[6, 7, 8], // Third row
[0, 4, 8], // Left-top to right-bottom diagonal
[2, 4, 6] // Right-top to left-bottom diagonal
];
export default class WinChecker {
checkWin (gameState) {
let reversedMoves = gameState.moves.slice(0).reverse();
let currentPlayersMoves = reversedMoves.filter((_, i) => !(i % 2));
return this.checkPlayerWin(currentPlayersMoves);
}
checkPlayerWin (currentPlayerMoves) {
return WIN_SCENARIOS.some((winMoves) => this.checkWinScenario(winMoves, currentPlayerMoves));
}
checkWinScenario (winMoves, currentPlayerMoves) {
return winMoves.every((move) => currentPlayerMoves.indexOf(move) !== -1);
}
}
A draw?
What happens if all the moves are done and no one has won?
Back in our game-state.js we need one last bit of code to handle a tied game. At the end of addMove, add the following:
if (!win && this.moves.length === 9) {
this.winner = null;
this.whoseTurn = null;
}
PHEW! That's almost it for the server.
Let's RECAP! π π π
We've done a heap of stuff!
We wrote a basic server!
We added some endpoints.
We added an internal representation of the game state.
We fleshed out our endpoints to update that state.
We added some validation to make sure a user couldn't enter bad data, or break our rules.
π
π
π
π
π
We wrote some code that works out if the game is over, and who won!
π
API DONE!
At this point we have a pretty fully featured API (Application Program Interface) for our game!
We could play a full game of tic-tac-toe using just curl if we wanted! But of course, we're fancier than that! π
WRITING THE CLIENT
Now we're going to write the app that consumes our API!
We're going to add a /client directory to our project, and add an index.html file like the following:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>TIC TAC TOE</title>
</head>
<body>
<h1>Tic Tac Toe</h1>
<main></main>
</body>
</html>
Serving our app
We need to make a small change to our server so it knows to serve all the static files from our /client directory.
We can replace the handler for the '/' path with a general static file server:
app.use(express.static('client'));
#soeasy πππ
REACT!
We're going to write our UI in React! However, there's a million other libraries we could have used, or we could've written it without any libraries at all.
The "best" front-end framework is a hotly debated topic - but it actually doesn't matter. Whatever you pick today will be totally wrong in 6 months anyway! π
πΏπΏπΏ
Client-Side Dependencies
<script src="https://cdn.jsdelivr.net/es6.shim/0.35.1/es6-shim.min.js"></script>
<script src="https://cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
<script src="https://npmcdn.com/react@15.3.0/dist/react.js"></script>
<script src="https://npmcdn.com/react-dom@15.3.0/dist/react-dom.js"></script>
<script src="https://npmcdn.com/babel-core@5.8.38/browser.min.js"></script>
We're going to add some third-party code that we want to use. This should go at the bottom of the <body> tag, below our <main> tag:
What we've included here are a few shims for new browser APIs, the code for React, and Babel again so we can use ES2015+ in our browser code too.
CSS
We're going to add some CSS just so that when we make our UI it looks a bit (like a tiny bit) better than just default HTML.
Create a styles.css file within the /client directory, and add the following to it:
@import 'https://fonts.googleapis.com/css?family=Slabo+27px';
html, body { margin: 0; min-width: 320px; text-align: center; }
body, input { font-family: 'Slabo 27px', serif; font-size: 20px; }
main { width: 100%; }
input { text-align: center; }
button {
width: 100px; height: 100px;
background: none; border: 1px black solid;
font-size: 50px; vertical-align: top;
}
We also need to add a reference to it in to our HTML, inside the <head> element:
<link rel="stylesheet" type="text/css" href="/styles.css">
TA DA!
If you jump to http://localhost:3000Β you should see a website!
There's not much here for now, but we're going to fix that.
π»
Our First Component
With React we build our UI from pieces of functionality called Components. Let's make our first one, which will manage most of the state of our application! We need to add a new file called tic-tac-toe.js inside our /client directory. To start with, it should look something like this:
class TicTacToe extends React.Component {
constructor () {
super();
}
render () {
return <div></div>
}
}
window.TicTacToe = TicTacToe;
β οΈ We're cheating a bit here - putting things on window is generally a bad idea. We should use proper modules.
Using our component
Now we need to include it in our page. Let's add a few more script tags to the bottom of our <body>
<script type="text/babel" src="/tic-tac-toe.js"></script>
<script type="text/babel">
ReactDOM.render(<TicTacToe/>, document.querySelector('main'));
</script>
βοΈNotice the "text/babel"? That's a pretty sweet hack that tells Babel that it needs to compile these scripts before they are run!
βοΈAlso see the weird HTML within our JavaScript? That's JSX, the templating language for React. We will see more of that later.
Fleshing It out a bit
This main component is going to do a few thing:
1β£οΈ
It's going to be responsible for getting the game state from the server.
2β£οΈ
It's going keep track of the token from the server that identifies a player.
3β£οΈ
And it's going to keep track of the game state and render the right thing.
constructor () {
super();
this.state = {
gameState: {
players: []
},
token: null
};
}
Let's start by setting the initial state in the tic-tac-toe.js file:
Getting the game State
loadGameState () {
fetch('/game-state')
.then(response => response.json())
.then(gameState => this.setState({ gameState }));
}
Let's add another function to our TicTacToe class:
Here we're using fetch, which is a new API that replaces the old janky XMLHttpRequest API. If you look back in our index.html file you'll see we had to include a polyfill for it - this is because it isn't implemented in all browsers yet.
Updating the APP State
Now we need to tell our component to actually do the call to get the data. So we need another function in TicTacToe:
componentDidMount () {
this.loadGameState();
setInterval(() => this.loadGameState(), 1000);
}
componentDidMount is part of the React component life-cycle. React will call this for us when the component has been successfully created. We are starting an interval that will request the updated game state every second.Β
If you refresh the page now, and open the Network tab of the developer tools, you should see the page making a request every second!
Rendering to the page
We want to get something to actually show up on the page now, so we need to add the most inportant method to the component - render:
render () {
let { gameState, token } = this.state;
let gameIsEmpty = gameState.players.length === 0;
let waitingForPlayer = gameState.players.length === 1;
let gameIsFull = gameState.players.length === 2;
let gameIsUnderway = gameIsFull && gameState.whoseTurn;
let gameIsWon = gameIsFull && !gameState.whoseTurn && gameState.winner;
let gameIsDrawn = gameIsFull && !gameState.whoseTurn && !gameState.winner;
}
For now, this doesn't actually render anything π , but let'sΒ take the opportunity to check out React's error messages. Reload the page and look in the console in Dev tools - it tells us we need to actually return HTML from our render function.
Joining the game
For our bits of state we defined before, we can work out when we want to show the user a form to join the game. Let's add the following to the end of the render method:
if (!token && !gameIsFull) {
return <JoinGame onJoinGame={token => this.setToken(token)}/>
}
Here you can see that pesky old JSX again. HTML in your JS looks a bit strange at first, but it solves a tricky problem in an interesting way.
<JoinGame> is going to be our next component.
Joining the game
Like before, we're going to add a new file inside the /client folder, this time called join-game.js:
class JoinGame extends React.Component {
constructor (props) {
super();
this.props = props;
this.state = {
name: '',
symbol: ''
};
}
render () {
return <div></div>
}
}
window.JoinGame = JoinGame;
This is much the same as last time, but with one main difference - the props parameter. This is how we can provide information to a component from the outside world.
USing The Component
Once again we need to add the script to our index.html file. Add the following before the tic-tic-toe.js script:
<script type="text/babel" src="/join-game.js"></script>
Let's refresh our page again and see if we get any errors!
Passing data to A component
Let's look at the render function of the TicTacToe class again, specifically the bit that uses the <JoinGame> component:
<JoinGame onJoinGame={token => this.setToken(token)}/>
All the extra stuff is what we want to pass through to the component from the outside. In this case, we are passing a function for it to use when the user has successfully joined the game. That function doesn't exist yet, so let's add it to the TicTacToe class:
setToken (token) {
this.state.token = token;
}
Joining the game
Let's tell the <JoinGame> component how to render itself by adding a render function:
render () {
return (
<form onSubmit={e => this.joinGame(e)}>
<h2>Join game:</h2>
<label>Name:</label>
<br/>
<input
type="text"
placeholder="Your name"
value={this.state.name}
onChange={e => this.handleNameChange(e)}
/>
<br/>
<label>Symbol:</label>
<br/>
<input
type="text"
placeholder="X"
value={this.state.symbol}
onChange={e => this.handleSymbolChange(e)}
/>
<br/>
<input type="submit" value="Join"/>
</form>
);
}
Joining the game
This looks a bit more like what we are used to, but there's still a few things worth pointing out.
We have some more event handlers being used:
onSubmit={e => this.joinGame(e)}
onChange={e => this.handleNameChange(e)}
onChange={e => this.handleSymbolChange(e)}
And we have some data binding:
value={this.state.name}
value={this.state.symbol}
Joining the game
Let's define those event handlers in our JoinGame class. First for our form values:
handleNameChange (e) {
this.setState({ name: e.target.value });
}
handleSymbolChange (e) {
this.setState({ symbol: e.target.value });
}
And also for our form submit:
joinGame (e) {
e.preventDefault();
fetch('/join-game', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(this.state)
})
.then(response => response.json())
.then(data => this.props.onJoinGame(data.token));
}
Whoop!
We should now be able to join the game! Let's reload the page and see what happens.
β οΈ Make sure you enter valid data, since we haven't got any client-side validation yet π
Client-side validation
Let's quickly update our code to use the error messages we send back from the server. Swap the final line of the joinGame method with the following:
.then(data => {
let { message, token } = data;
if (message) {
alert(message);
} else {
this.props.onJoinGame(token)
}
});
Nothing fancy, but if we get a message back from the server, at least the user will see it!
Waiting...
Cool, now we can join a game! We now need a state for when you're waiting for another player to join. Let's add the following to the end of the render method in the TicTacToe class:
if (token && waitingForPlayer) {
return <h2>Waiting for player...</h2>
}
Try it out by opening the page in two browser tabs, and joining the game from one, and then the other!
Playing the game
Now for the fun bit! Actually making the game work!
First let's add some code to render our new state (again in the render function of the TicTacToe class):
if (gameIsUnderway) {
return <Board token={token} gameState={gameState}></Board>
}
You can see we've got a new component, and this time we're passing two bits of data to it, the token that allows us to make moves, and the game state.
The Board Component
Let's do the same thing as before and create a new file called board.js:
class Board extends React.Component {
constructor (props) {
super();
this.state = {
gameState: props.gameState,
token: props.token
};
}
render () {
return <div></div>
}
}
window.Board = Board;
And let's add it to the index.html file again, above the join-game.js script.
<script type="text/babel" src="/board.js"></script>
Rendering the board
Once again we're going to need a render function:
render () {
let { gameState, token } = this.state;
let { whoseTurn } = this.state.gameState;
let boardState = this.getBoardState(this.state.gameState);
return (
<div>
<h2>Next move: {whoseTurn.name}</h2>
<button onClick={() => this.takeTurn(0)}>{boardState[0]}</button>
<button onClick={() => this.takeTurn(1)}>{boardState[1]}</button>
<button onClick={() => this.takeTurn(2)}>{boardState[2]}</button>
<br/>
<button onClick={() => this.takeTurn(3)}>{boardState[3]}</button>
<button onClick={() => this.takeTurn(4)}>{boardState[4]}</button>
<button onClick={() => this.takeTurn(5)}>{boardState[5]}</button>
<br/>
<button onClick={() => this.takeTurn(6)}>{boardState[6]}</button>
<button onClick={() => this.takeTurn(7)}>{boardState[7]}</button>
<button onClick={() => this.takeTurn(8)}>{boardState[8]}</button>
</div>
);
}
Taking a turn
We need a few functions to make this work. First, we need one to actually tell the server that we've made a move:
takeTurn (move) {
let { token } = this.state;
if (token) {
fetch('/take-turn', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ move, token })
})
.then(response => response.json())
.then(data => {
let { message } = data;
if (message) {
alert(message);
}
});
}
}
Note that a token is required to make a move! This means we can have spectators, but if they click the board nothing will happen.
Updating the board
First of all, we need to tell our component to update its state when new data comes in from outside:
componentWillReceiveProps (nextProps) {
this.setState({ gameState: nextProps.gameState });
}
This is another part of the React component life-cycle. This function is called when the data that is passed into the component changed. It is up to us to tell the inner component to update its state.
Updating the board
Now we do some magic to turn the gameState from the server into the moves on the board!
Let's add a getBoardState method to the Board component:
getBoardState (gameState) {
let { moves, players, whoseTurn } = gameState;
let boardState = [null, null, null, null, null, null, null, null, null];
let otherPlayer = players.find(player => {
// Hack ahoy!
return JSON.stringify(player) !== JSON.stringify(whoseTurn)
});
let reversedMoves = moves.slice(0).reverse();
reversedMoves.map((move, i) => {
boardState[move] = i % 2 === 0 ? otherPlayer.symbol : whoseTurn.symbol;
});
return boardState;
}
Updating the board
let boardState = [null, null, null, null, null, null, null, null, null];
Let's go through the important bits of that line by line. We initialise the board state to 9 null values:
Then we figure out who the other player is by looking for the player who isn't the current player. We stringify here to compare the values, since JavaScript object comparison is done by reference (there's better ways to do this, I promise!)
let otherPlayer = players.find(player => {
// Hack ahoy!
return JSON.stringify(player) !== JSON.stringify(whoseTurn);
});
Updating the board
Lastly, for each move that has been made, we alternate between setting that square to the symbol of each player:
let reversedMoves = moves.slice(0).reverse();
reversedMoves.map((move, i) => {
boardState[move] = i % 2 === 0 ? otherPlayer.symbol : whoseTurn.symbol;
});
#simpleπ π π #notmagic
The end of the game
Now all that's left to do is add states for the end of the game. Let's update the render function of TicTacToe for the last time, by adding the following to the end:
if (gameIsWon) {
return <h2>{gameState.winner.name} won!</h2>
}
if (gameIsDrawn) {
return <h2>Draw!</h2>
}
et voila! Our game should now be playable start to end!
Let's RECAP (AGAIN)! π π π
We've done another heap of stuff!
We made our server server static files
We wrote some HTML
We added some polyfills and dependencies
We wrote a UI for our game, including three React components!
π
π
π
π
WHAT's Next!?
There's still heaps of stuff we could do here!
We could make it prettier?
We could fix the bug where both players can have the same symbol!
We could let you use an emoji for your symbol!?
We could make it so you don't have to restart the server to start a new game π
π€
π€
π€
π€
FIN! THANK YOU!
π
summer-of-tech-js-masterclass
By Craig Spence
summer-of-tech-js-masterclass
Summer of Tech 2016 - JS Masterclass
- 4,037