Articles tagged 'frontend'

Tic-Tac-Toe with Vue.js Jan 17 2018

Following on from our look at setting up the Vue.js and React frameworks in Rails 5.1, this post will look at building a small little component in Vue.js, strictly on the client-side (just HTML, JS and CSS, no backend Rails app!).

We're going to implement our own little version of the classic Tic-Tac-Toe game, called Tic-Tac-Vue.

Setup

We'll start by creating our structure - create a folder, an HTML page, a JS file, and a stylesheet.

mkdir tic-tac-vue
cd tic-tac-vue
touch index.html
touch app.js
touch app.css

Our HTML file is going to be very basic, let's run through our initial markup:

<html>
  <head>
    <title>Tic Tac Vue</title>
    <script src="http://unpkg.com/vue"></script>
    <link rel="stylesheet" href="./app.css" />
  </head>
  <body>
    <script src="./app.js" type="text/javascript"></script>
  </body>
</html>

This sets up our page, brings in Vue.js, our CSS and our own JS file (both currently empty).

Next up, we're going to add a root element for our app to live - this goes in our HTML body, above our script reference to our JS file:

<div id="app"></div>

And now we need the markup for our game grid itself, which we're going to put in a script tag with a special template type - this too goes in our body, and above our JS file script ref:

<script type="text/x-template" id="grid">
  <div class="grid">
    <div class="row" v-for="row in rows">
      <div class="column" v-for="column in row">
        {{column}}
      </div>
    </div>
  </div>
</script>

Let's run through that so it's clear - our grid is made up of rows and columns, fairly simple. It'll be based on a 2D array representing that data, and so we're looping through our rows data, which is an array of arrays, and then looping through each individual array, which contains the values for our individual columns. We'll be expecting three rows, and three columns for Tic-Tac-Toe of course. The column values will either be blank (unplayed), or an x or an o if someone has played in that space. That's it!

Quick note: Why do we separate the markup like this? You could include that markup within the #app div directly when the app is so simple, however there are still a couple of downsides to this. The first is that you have to then be careful which markup you're using - the browser will process that as HTML within the DOM long before Vue gets to it, meaning that if you then start to use any markup specific to how Vue works (like referencing components with tags) that isn't HTML5 compliant, the browser could end up stripping it out, or doing odd things with it that'll affect how your app works. Additionally, most apps are going to have enough complexity to have at least one component (rather than defining the app as just a root Vue app), so it makes sense to define that as such from the beginning, as it's neater and clearer to follow, as well as more cleanly allowing for expansion as the need arises. We've separated our page container markup (to host our client-side app) from the app itself, which is comprised of our specific Vue markup template, and the JavaScript we'll be writing to implement the functionality itself.

With our HTML out of the way, let's format our grid so it looks like one - here is our CSS:

.row {
  clear: both;
}
.column {
  border: 1px solid #000;
  width: 50px;
  height: 35px;
  float: left;
  text-align: center;
  padding-top: 15px;
}

Fairly simple - we're floating each column and having them on the same row, with each row clearing the float to move on to the next row - we setup a border for the grid itself, set a specific height and width, and make sure that if a text value is shown (x or o) it looks nice within the box for that space on the game grid.

Now we get to the fun part - our JavaScript! We have two tasks now to get our initial grid functional - we have to create and define our grid as a component, and we then have to create our Vue app with our grid component being rendered into our #app empty div above.

First, we define our grid component:

var Grid = Vue.component('grid', {
  template: '#grid'
});

This is easy enough to follow - we define a component, named Grid, using the template with id grid, and store it as Grid for reference elsewhere.

We're going to need to set our initial data for the component though - the rows and columns that the template renders:

data: function() {
    return {
      rows: [
        ['', '', ''],
        ['', '', ''],
        ['', '', '']
      ]
    };
  }

As we described above for our template, rows is a 2D array, an array of arrays, each array representing a row, and each value within representing a column in that row - and we're starting with them all blank and unplayed.

Next up, let's define our App:

var App = new Vue({
  el: '#app',
  components: {
    Grid
  },
  render: function(createElement) {
    return createElement(Grid);
  }
});

Here we specify the element our app will reside in, so #app, and then the components we need to use - we just have one, our Grid component. Lastly, as we don't have any markup within our #app element to tell our app how and what to render, we do it with a custom render method, referencing our game grid component.

Open up index.html now in a browser and you'll see our grid! That's great, but it doesn't do a lot. Let's start making it so we can play the spaces, and get a game going!

Game on

First of all, we want the user to be able to select a space, and if it's empty, it'll place an x or an o in it, depending upon whose turn it is. Let's add a variable to our data to determine whose go it is, and which one is next:

  data: function() {
    return {
      rows: [
        ['', '', ''],
        ['', '', ''],
        ['', '', '']
      ],
      next: 'x'
    };
  },

So we've just added that next attribute with a default value of x to our component data.

Now we'll add a method to our component definition, that'll handle when a space is tapped:

methods: {
  tap: function(e) {
    if (e.target.innerText == '') {
      e.target.innerText = this.next;
      this.next = (this.next == 'x' ? 'o' : 'x');
    }
  }
}

So it only works if the space is free, sets the space, and then toggles which player is next. Reload our page, tap a space, and… nothing. That's because we have one final step - we need to hook up tapping on the game grid spaces to that method we just wrote!

Change the markup in our #grid component template, so it's like this:

<div class="column" v-for="column in row" v-on:click="tap">
  {{column}}
</div>

The v-on:click attribute is added, which adds an event handler for the click event to the component, pointing at the tap method. Try reloading again now, et voila! Tapping on spaces sets the next player character in the space, with no overwriting involved. Pretty neat, and not a lot of code either!

Data binding

We have our action in place, but we can't do much with spaces played as we're not updating our data based on the user input - we're just directly updating the UI. Instead, let's update our data array, and have that reflect to the UI, which is a much better way to handle things.

In our markup, we'll alter our Grid component template as follows:

<script type="text/x-template" id="grid">
  <div class="grid">
    <div class="row" v-for="(row, row_index) in rows">
      <div class="column" v-for="(column, column_index) in row" v-on:click="tap" v-bind:data-row="row_index" v-bind:data-column="column_index">
        {{column}}
      </div>
    </div>
  </div>
</script>

Specifically, we're keeping track of the row and column index in our two-dimensional array looping, and then binding it as row and column data variables to reference when a space is played.

Over to our tap method now:

tap: function(e) {
  if (e.target.innerText == '') {
    let rows = this.rows;
    rows[e.target.attributes['data-row'].value][e.target.attributes['data-column'].value] = this.next;
    this.rows = rows.slice(0);
    this.next = (this.next == 'x' ? 'o' : 'x');
  }
}

Here we are grabbing the data rows, then updating the value of our column, using the data-row and data-column bindings present on the element tapped to find the right location in the array. The reason we do a slice(0) is because the way that the data binding works in Vue, the rows property in our data is reactive (meaning it is being watched for changes), but individual changes within the array won't trigger that watcher, and therefore a re-render. Instead we're using a local rows variable, and then effectively copying the whole array back into our data, which triggers our reaction and an update of the UI using our template (rather than us modifying the element directly).

So now we have our app data binding correctly, we can implement logic based on the current rows/columns data to check for win criteria!

Winner winner

To check for a winner, we need to check each row for three matching values, each column for three matching values, and the two diagonal paths for three matching values. If any of those are true, we have a winner! We can boil this down to the following:

checkWinner: function() {
  return (
    this.checkValues(this.rows[0]) ||
    this.checkValues(this.rows[1]) ||
    this.checkValues(this.rows[2]) ||
    this.checkValues([this.rows[0][0], this.rows[1][0], this.rows[2][0]]) ||
    this.checkValues([this.rows[0][1], this.rows[1][1], this.rows[2][1]]) ||
    this.checkValues([this.rows[0][2], this.rows[1][2], this.rows[2][2]]) ||
    this.checkValues([this.rows[0][0], this.rows[1][1], this.rows[2][2]]) ||
    this.checkValues([this.rows[0][2], this.rows[1][1], this.rows[2][0]]));
},
checkValues: function(values) {
  return (values[0] != '' && values[1] != '' && values[2] != '' && (values[0] === values[1]) && (values[1] === values[2]));
}

Add those methods to our component underneath our tap definition.

checkValues is easy enough - we expect an array of three values, we make sure all the values are not blank, and that they are the same. checkWinner then calls checkValues for all of our different possible value arrays - firstly checking each of the three rows (each of those is already an array with three elements), then the three columns, and then lastly, the two diagonals.

With our win logic in place, we now just need to hook it up. We'll add an additional property, finished, to our data:

data: function() {
  return {
    rows: [
      ['', '', ''],
      ['', '', ''],
      ['', '', '']
    ],
    next: 'x',
    finished: false
  };
},

We'll then reference that to make sure that you can't keep playing once we're finished - at the top of the tap method:

tap: function(e) {
  if (!this.finished && e.target.innerText == '') {

Lastly in terms of logic, we just need to amend the end of our tap method:

if (this.checkWinner()) {
  this.finished = true;
} else {
  this.next = (this.next == 'x' ? 'o' : 'x');
}

So we only go to our next player if we've not found a winner - if we have found a winner, we mark the game as finished.

Playing the game now, we'll see that if one player wins, you can't continue playing. But some kind of feedback to indicate that the game is over (and to say who has won!) would be good. Easy! We can do that with a single additional line in our template - at the bottom within our .grid div, below the rows & columns, add:

<div class="status" v-if="finished">Finished! {{next}} wins!</div>

Add the following to app.css to make sure our message sits underneath the grid:

.status {
  clear: both;
}

Now when someone wins and the game stops, you can see who has won!

Whose turn?

It'd be good if we could show whose turn it is next, in case the players lose track. We already have the data, and as you can probably guess from how we displayed the finish state, it's fairly easy to do - change our .status text div to the following:

<div class="status">
  <span v-if="finished">Finished! {{next}} wins!</span>
  <span v-else>Next go: {{next}}</span>
</div>

Here we're making use of v-if and v-else to make sure only one shows - either we're showing whose turn is next, or we show the finished/winner text as the game is over!

Replayability

We're almost there with our incredible take on Tic-Tac-Toe, but rather than having to refresh the page after every game to go again, wouldn't it be cool if we could just press a restart button instead?

We're going to move our code to determine the next player into its own method, so we can re-use it:

nextPlayer: function() {
  this.next = (this.next == 'x' ? 'o' : 'x');
},

Next up, we'll change the tap method to use that function - so for the winner check or next player logic:

if (this.checkWinner()) {
  this.finished = true;
} else {
  this.nextPlayer();
}

Now, we'll implement our restart method:

restart: function(e) {
  this.rows = [
    ['', '', ''],
    ['', '', ''],
    ['', '', '']
  ];
  this.finished = false;
  this.nextPlayer();
},

This just resets the rows, makes sure the game is no longer finished, and triggers the next player. Let's hook that up to something in the template now:

<div class="status">
  <div v-if="finished">
    <p>Finished! {{next}} wins!</p>
    <a v-on:click="restart">Restart</a>
  </div>
  <span v-else>Next go: {{next}}</span>
</div>

Our restart link shows when the game is finished, and tapping it resets our game accordingly!

Stalemate

The last thing we need to handle, is the dreaded stalemate. When two world class Tic-Tac-Toe players are locked together, head-to-head, neither willing to budge an inch, we could end up with a tied game. What then? The game doesn't get marked as finished, but there are no more moves to make. The games cannot continue!

So let's add a check for that state, and handle it accordingly. We'll start by refactoring our checkValues method, so we can re-use the blank check within it for checking if all the spaces have been played:

checkValues: function(values) {
  return this.checkValuesPresent(values) && this.checkValuesMatch(values);
},
checkValuesPresent: function(values) {
  return (values[0] != '' && values[1] != '' && values[2] != '');
},
checkValuesMatch: function(values) {
  return (values[0] === values[1]) && (values[1] === values[2]);
}

As you can see, we've separated the two parts of checkValues into a check for whether the specified values are all present (checkValuesPresent) and whether the values all match (checkValuesMatch). Now we can use the checkValuesPresent check in our stalemate logic - add the following method:

checkStalemate: function() {
  return !this.finished &&
    (this.checkValuesPresent(this.rows[0]) &&
    this.checkValuesPresent(this.rows[1]) &&
    this.checkValuesPresent(this.rows[2]));
},

We're just making sure that the game isn't yet finished, and all of the rows have values present - then it must be a stalemate!

We'll add an additional property to track stalemates, so add the default value to our data:

next: 'x',
finished: false,
stalemate: false

And then we'll update our tap method to account for checking for stalemates:

if (this.checkWinner()) {
  this.finished = true;
} else if (this.checkStalemate()) {
  this.stalemate = true;
  this.finished = true;
} else {
  this.nextPlayer();
}

We have to check for a stalemate after a win, because there might be a winner on the very last space! If it is a stalemate, the game is finished, but also the stalemate property is set.

We can now use this on the frontend within the template to display something more relevant:

<div class="status">
  <div v-if="finished">
    <p v-if="stalemate">It's a draw! Stalemate!</p>
    <p v-else>Finished! {{next}} wins!</p>
    <a v-on:click="restart">Restart</a>
  </div>
  <span v-else>Next go: {{next}}</span>
</div>

We're either displaying our draw statement for stalemates, or the winner text, and the restart button either way so a new game can start. The only thing left, is to make sure our stalemate flag is reset on restart:

restart: function(e) {
  this.rows = [
    ['', '', ''],
    ['', '', ''],
    ['', '', '']
  ];
  this.finished = false;
  this.stalemate = false;
  this.nextPlayer();
},

Now win, lose or draw, players can continue playing!

Wrapping up

So our Tic-Tac-Toe game is pretty much feature complete, at least in terms of a basic game two people can play on a single computer. It saves on paper and drawing, and we've done it all with just a little HTML + CSS, and a bit of fun JavaScript, all thanks to Vue.js!

This is one way to build a Vue.js app, and is better suited for small, self-contained ideas like this. In a future post we'll look at going back to Webpack and using vue-cli, so that we can do things like having each component of our app (template HTML and JS logic) in its own .vue file, that's pre-processed accordingly to build the JS for our page. As projects increase in complexity, that helps to give them structure and keep them maintainable.

Check out the full code for this post at https://github.com/ejdraper/tic-tac-vue, and you can play the game itself below!

Comments

Frontend Fun with Rails 5.1 and Webpacker Jan 8 2018

Client side frameworks for more responsive web apps are en vogue, and have been for a number of years now. The frameworks themselves change in popularity - to begin with it was Backbone, then Ember and Angular - more recently, frameworks like Vue and React have become the most popular choices.

Let it never be said that Rails doesn't move with the times though, as Rails 5.1 introduced the Webpacker gem, providing a smarter interface to allow you to use Webpack config for bundling JavaScript packages on the client side of your Rails web app. This means easier access to stable versions of client side frameworks, and easier maintainability for those dependencies going forward. It's a separate gem too, allowing you to also use it in Rails 4.2+ apps if needed.

Let's take a look at just how easy it is in Rails 5.1 now to setup Vue.js and React. Make sure you have the latest version of Rails installed, and then we'll setup a new empty app:

rails new frontend-fun

You can also specify to use webpack as part of this command and skip the next couple of steps:

rails new frontend-fun --webpack

However, if you're looking to add this to an existing Rails app, these next two steps will allow you to add Webpacker ready to use.

Within the app, add the webpacker gem to Gemfile:

gem "webpacker"

Then run bundle from a terminal in the root directory of the project to install the gem:

bundle install

At this point, we'll now have some additional commands available to us courtesy of the webpacker gem, to install frontend packages. We'll get things rolling with the initial command to prep our webpacker config:

 bin/rails webpacker:install

With that done, we should probably setup a basic page for testing, so create a new controller app/controllers/site_controller.rb, with the following Ruby code in it:

class SiteController < ApplicationController
end

Then we'll create a template for our content, at app/views/site/index.html.erb:

<h1>FrontendFun</h1>

Finally, in our config/routes.rb file, we'll add a reference for the root of our app to the new controller and action:

Rails.application.routes.draw do
  root "site#index"
end

If you test our app now, you'll see the FrontendFun header. Last thing we now need to do is hook in our webpacker JS, with a reference from our layout template. In app/views/layouts/application.html.erb, below the javascript_include_tag reference in the <head>, add:

<%= javascript_pack_tag 'application' %>

Now if you reload the page, and look in the JavaScript console, you should see:

Hello World from Webpacker

Webpacker - it's alive!

Vue.js

To install Vue.js, run the following commands:

bin/rails webpacker:install:vue
bin/yarn install

This will install the Vue.js packages. We'll then hook up the Hello Vue pack that is setup by default, by modifying our javascript_pack_tag line in app/views/layouts/application.html.erb:

<%= javascript_pack_tag 'application', 'hello_vue' %>

Reload our page, and you'll now see Hello Vue! in our page body, which is coming directly from Vue.js!

React

Now, in a production application, you're unlikely to want to use more than one frontend framework, unless you were transitioning from one to another. But, this is FrontendFun, so we're going to add React too!

By now, this should be fairly straightforward, we run:

bin/rails webpacker:install:react
bin/yarn install

This will install the React packages now, and setup a base HelloReact component similar to our HelloVue. So we'll just need to modify our javascript_pack_tag one more time to reference our React pack:

<%= javascript_pack_tag 'application', 'hello_vue', 'hello_react %>

Once more, reload our page, and behold - Hello Vue! and Hello React!.

Two frontend frameworks, setup very easily, co-existing in the same HTML page in our Rails app. The possibilities are practically endless!

Next steps

From here, you can edit either app/javascripts/packs/hello_vue.js and app/javascripts/app.vue if you want to use Vue.js, or app/javascripts/hello_react.jsx for React, adding new components and interactions to build out your frontend, and integrate it with whatever backend functionality your Rails app might offer. You can check out the simple demo Rails app that we've just built up here.

Comments

Page 1 of 1 |