Creating a Crossword Puzzle game with React.JS – Part 2

In this second part of this tutorial, I will complete the Crossword Puzzle game.

Like in part 1, we will continue to use JSFiddle for testing purposes.  In this part, we will complete this crossword game by handling events for user input.

Learn React online

If you are keen to learn React from the ground-up feel free to check Learn and Understand React JS on Zenva Academy which covers all the basics + lots of bonus topics like React Router and Flux.

Download the source code

You can download all the files associated with this tutorial here

Composing Components

The ability to nest components within other components is a very important feature of React.  Because components should be completely self-contained, any component can be nested inside any other component.

In our game, we have composed quite a few components.  In Game class, we composed the Clues component and in Clues class, we composed the Output component.

var Game = React.createClass({
    render: function() {
        ...
        <div className="col-md-4">
            <h2>Clues</h2>
            <Clues data={this.props.data} />
        </div>
        ...
    }
});

var Clues = React.createClass({
    render: function() {
        var statements = this.props.data.map(function(clues) {
            return (
                <Output clue={clues.clue}></Output>
            );
        });
        ...
});

var Output = React.createClass({
    render: function() {
        return (
            <div className="clues">
                <p>{this.props.clue}</p>
            </div>
        );
    }
});

We used map, which takes each item of an array, performs some transformation on it, and returns a new array containing the transformed elements.  Testing on JSFiddle, we see that we can get a particular value from an array using the props command.

var Game = React.createClass({
  render: function() {
    return (
      <div>
        <div>
          <h3>Crossword</h3>
          <p>{this.props.data[0].answer}</p>
        </div>
        <div>
          <h3>Clues</h3>
          <Clues content={this.props.data[0].clue} />
        </div>
      </div>
    );
  }
});

var Clues = React.createClass({
  render: function() {
    return (
      <div>
        <p>{this.props.content}</p>
      </div>
    );
  }
});

While this works for single array value, we are hard-coding the Game component.  To handle more than just a set number of clues, we need to change that by mapping each clue to a new Clues component with its content attribute correctly set to the clue value.

So, our code can be one component shorter.

var Game = React.createClass({
  render: function() {
    return (
      <div>
        <div>
          <h3>Crossword</h3>
          <p>{this.props.data[0].answer}</p>
        </div>
        <div>
          <h3>Clues</h3>
          {this.props.data.map(function (clues){
            return <Clues content={clues.clue} />
          })}
        </div>
      </div>
    );
  }
});

Events

React has a simple, consistent, normalized event system.  Just as React has its own DOM abstract, it also has its own event abstraction.  This is used to normalize the event behavior, as React makes events behave the same across all supported browsers.  React also creates a uniform event system for DOM event objects, replacing them with a wrapper called SyntheticEvent.

DOM events are the events generated by the browser, things like button click events, changing of text inputs, and form submission events.  DOM events are the events that receive a React SyntheticEvent object, which is a normalized event object across all browsers.

Let’s try to handle an input change event in JSFiddle.  This example, has an input text box and what the user types will be echoed into a label.  Much like the behavior in AngularJS.

var Echo = React.createClass({
  getInitialState: function() {
    return {value: ''};
  },
  handleChange: function(e) {
    this.setState({value: e.target.value});
  },
  render: function() {
    return (
      <div>
        <input type="text" placeholder="Enter your name" onChange={this.handleChange} />
        <br />
        <label>Hello {this.state.value}!</label>
      </div>
    );
  }
});

ReactDOM.render(
  <Echo />,
  document.getElementById('container')
);

We initialized the variable value to an empty string.  We included an input textbox which has an onChange attribute as well as a label component that outputs what you typed in the textbox as you type it in.  The onChange event handler, named handleChange, merely sets the state of the value object.

In the puzzle game, I’ve removed the handleChange event handler I’ve had in the input in the first part of this tutorial, and instead, added a Button which has a clickHandler event.

<button className="btn btn-lg btn-primary" style={toRight} onClick={this.clickHandler}>Check answers</button>

In the clickHandler function, I created some variables to extract the letters from the answer in data.

clickHandler: function(e) {
	var letter0016 = this.props.data[0].answer.charAt(0),
	letter0116 = this.props.data[0].answer.charAt(1),
	letter0216 = this.props.data[0].answer.charAt(2),
	letter0316 = this.props.data[0].answer.charAt(3),
	letter0416 = this.props.data[0].answer.charAt(4),
	letter0516 = this.props.data[0].answer.charAt(5),
	letter0616 = this.props.data[0].answer.charAt(6),
	letter0716 = this.props.data[0].answer.charAt(7),
	letter0816 = this.props.data[0].answer.charAt(8);
	var letter0311 = this.props.data[1].answer.charAt(0),
	letter0411 = this.props.data[1].answer.charAt(1),
	letter0511 = this.props.data[1].answer.charAt(2),
	letter0611 = this.props.data[1].answer.charAt(3),
	letter0711 = this.props.data[1].answer.charAt(4),
	letter0811 = this.props.data[1].answer.charAt(5),
	letter0911 = this.props.data[1].answer.charAt(6),
	letter1011 = this.props.data[1].answer.charAt(7),
	letter1111 = this.props.data[1].answer.charAt(8),
	letter1211 = this.props.data[1].answer.charAt(9),
	letter1311 = this.props.data[1].answer.charAt(10),
	letter1411 = this.props.data[1].answer.charAt(11),
	letter1511 = this.props.data[1].answer.charAt(12),
	letter1611 = this.props.data[1].answer.charAt(13),
	letter1711 = this.props.data[1].answer.charAt(14),
	letter1811 = this.props.data[1].answer.charAt(15);
	var letter0503 = this.props.data[2].answer.charAt(0),
	letter0603 = this.props.data[2].answer.charAt(1),
	letter0703 = this.props.data[2].answer.charAt(2),
	letter0803 = this.props.data[2].answer.charAt(3),
	letter0903 = this.props.data[2].answer.charAt(4),
	letter1003 = this.props.data[2].answer.charAt(5),
	letter1103 = this.props.data[2].answer.charAt(6),
	letter1203 = this.props.data[2].answer.charAt(7),
	letter1303 = this.props.data[2].answer.charAt(8);
	var letter0512 = this.props.data[3].answer.charAt(1),
	letter0513 = this.props.data[3].answer.charAt(2),
	letter0514 = this.props.data[3].answer.charAt(3),
	letter0515 = this.props.data[3].answer.charAt(4),
	letter0517 = this.props.data[3].answer.charAt(6),
	letter0518 = this.props.data[3].answer.charAt(7),
	letter0519 = this.props.data[3].answer.charAt(8),
	letter0520 = this.props.data[3].answer.charAt(9);
	var letter0600 = this.props.data[4].answer.charAt(0),
	letter0601 = this.props.data[4].answer.charAt(1),
	letter0602 = this.props.data[4].answer.charAt(2),
	letter0604 = this.props.data[4].answer.charAt(4),
	letter0605 = this.props.data[4].answer.charAt(5),
	letter0606 = this.props.data[4].answer.charAt(6),
	letter0607 = this.props.data[4].answer.charAt(7),
	letter0608 = this.props.data[4].answer.charAt(8);
	var letter0809 = this.props.data[5].answer.charAt(0),
	letter0810 = this.props.data[5].answer.charAt(1),
	letter0812 = this.props.data[5].answer.charAt(3),
	letter0813 = this.props.data[5].answer.charAt(4),
	letter0814 = this.props.data[5].answer.charAt(5),
	letter0815 = this.props.data[5].answer.charAt(6),
	letter0817 = this.props.data[5].answer.charAt(8),
	letter0818 = this.props.data[5].answer.charAt(9);
	var letter1005 = this.props.data[6].answer.charAt(0),
	letter1006 = this.props.data[6].answer.charAt(1),
	letter1007 = this.props.data[6].answer.charAt(2),
	letter1008 = this.props.data[6].answer.charAt(3),
	letter1009 = this.props.data[6].answer.charAt(4),
	letter1010 = this.props.data[6].answer.charAt(5);
	var letter1202 = this.props.data[7].answer.charAt(0),
	letter1204 = this.props.data[7].answer.charAt(2),
	letter1205 = this.props.data[7].answer.charAt(3),
	letter1206 = this.props.data[7].answer.charAt(4),
	letter1207 = this.props.data[7].answer.charAt(5),
	letter1208 = this.props.data[7].answer.charAt(6),
	letter1209 = this.props.data[7].answer.charAt(7),
	letter1210 = this.props.data[7].answer.charAt(8);
	var letter1507 = this.props.data[8].answer.charAt(0),
	letter1607 = this.props.data[8].answer.charAt(1),
	letter1707 = this.props.data[8].answer.charAt(2),
	letter1807 = this.props.data[8].answer.charAt(3),
	letter1907 = this.props.data[8].answer.charAt(4),
	letter2007 = this.props.data[8].answer.charAt(5);
	var letter1704 = this.props.data[9].answer.charAt(0),
	letter1705 = this.props.data[9].answer.charAt(1),
	letter1706 = this.props.data[9].answer.charAt(2),
	letter1708 = this.props.data[9].answer.charAt(4),
	letter1709 = this.props.data[9].answer.charAt(5),
	letter1710 = this.props.data[9].answer.charAt(6),
	letter1712 = this.props.data[9].answer.charAt(8),
	letter1713 = this.props.data[9].answer.charAt(9);
...

Forms

React has some very basic form support built-in, which includes binding values to the form, binding values back out of the form and accessing individual form elements.  In this Puzzle game, each white box has an input element within them.  The input boxes are there for the input of a character for the crossword.

<td className="cell cell16">
  <input type="text" 
      ref="answer0016" 
      placeholder="1" 
      maxLength="1" />
</td>

You will notice there is no value attribute for each <input>.  If the <input> element has value, then it would have been a controlled component.  We are not required to have a controlled component, in this instance.

Let’s take a simple forms input example to test on JSFiddle.

var Puzzle = React.createClass({
  render: function() {
    return <input type="text" value="Kingdom" />;
  }
});

With the value of the input being set, you have controlled this component.  This does not allow you to change the value in the input box at all.  By adding an onChange event handler to the input box, React triggers the onChange event and logs the value shown in the input box as well as whatever character you type.

var Puzzle = React.createClass({
  handleChange: function(e) {
    console.log(e.target.value);
  },
  render: function() {
    return <input type="text" value="Kingdom" onChange={this.handleChange} />;
  }
});

As you type a character, anywhere in the input box, you will notice that the changed value is being written to the console.  To view the changes in the console, open your Developer Tools in Chrome or Web Console in Firefox.

screenshot-01_pt2

However, this immediately triggers a re-render for the component which resets the value.  From the user’s point of view, they are typing, but nothing is happening.  The secret to making this work is to populate the input boxes value from the owning component’s state.  This is consistent with the React component design guidance because the value of the input box can change, therefore, it is state.

To allow the user to change the content of the input box we need to update the onChange handle to modify the state.  We also initialize the value in a getInitialState function.

var Puzzle = React.createClass({
  getInitialState: function() {
    return {value: 'Kingdom'};
  },
  handleChange: function(e) {
    this.setState({value: e.target.value});
  },
  render: function() {
    var value = this.state.value;
    return <input type="text" value={value} onChange={this.handleChange} />;
  }
});

ReactDOM.render(
  <Puzzle />,
  document.getElementById('container')
);

Since we will be rendering our components without a value, this is referred to as uncontrolled components.  Using uncontrolled components tells React to exempt the value of the control from the usual rendering process.  We can still handle the onChange event to detect changes to the input, however.  If you still want to initialize the component with a non-empty value, you can supply a defaultValue prop, which gives the input box a starting value.

var Puzzle = React.createClass({
  getInitialState: function() {
    return {value: ''};
  },
  render: function() {
    var value = this.state.value;
    return <input type="text" defaultValue={value} onChange={this.handleChange} />;
  }
});

Now to complete the onClick event handler.

clickHandler: function(e) {
...
  var word1Letter1 = ReactDOM.findDOMNode(this.refs.answer0016).value.toUpperCase(),
	word1Letter2 = ReactDOM.findDOMNode(this.refs.answer0116).value.toUpperCase(),
	word1Letter3 = ReactDOM.findDOMNode(this.refs.answer0216).value.toUpperCase(),
	word1Letter4 = ReactDOM.findDOMNode(this.refs.answer0316).value.toUpperCase(),
	word1Letter5 = ReactDOM.findDOMNode(this.refs.answer0416).value.toUpperCase(),
	word1Letter6 = ReactDOM.findDOMNode(this.refs.answer0516).value.toUpperCase(),
	word1Letter7 = ReactDOM.findDOMNode(this.refs.answer0616).value.toUpperCase(),
	word1Letter8 = ReactDOM.findDOMNode(this.refs.answer0716).value.toUpperCase(),
	word1Letter9 = ReactDOM.findDOMNode(this.refs.answer0816).value.toUpperCase(),
	word2Letter1 = ReactDOM.findDOMNode(this.refs.answer0311).value.toUpperCase(),
	word2Letter2 = ReactDOM.findDOMNode(this.refs.answer0411).value.toUpperCase(),
	word2Letter3 = ReactDOM.findDOMNode(this.refs.answer0511).value.toUpperCase(),
	word2Letter4 = ReactDOM.findDOMNode(this.refs.answer0611).value.toUpperCase(),
	word2Letter5 = ReactDOM.findDOMNode(this.refs.answer0711).value.toUpperCase(),
	word2Letter6 = ReactDOM.findDOMNode(this.refs.answer0811).value.toUpperCase(),
	word2Letter7 = ReactDOM.findDOMNode(this.refs.answer0911).value.toUpperCase(),
	word2Letter8 = ReactDOM.findDOMNode(this.refs.answer1011).value.toUpperCase(),
	word2Letter9 = ReactDOM.findDOMNode(this.refs.answer1111).value.toUpperCase(),
	word2Letter10 = ReactDOM.findDOMNode(this.refs.answer1211).value.toUpperCase(),
	word2Letter11 = ReactDOM.findDOMNode(this.refs.answer1311).value.toUpperCase(),
	word2Letter12 = ReactDOM.findDOMNode(this.refs.answer1411).value.toUpperCase(),
	word2Letter13 = ReactDOM.findDOMNode(this.refs.answer1511).value.toUpperCase(),
	word2Letter14 = ReactDOM.findDOMNode(this.refs.answer1611).value.toUpperCase(),
	word2Letter15 = ReactDOM.findDOMNode(this.refs.answer1711).value.toUpperCase(),
	word2Letter16 = ReactDOM.findDOMNode(this.refs.answer1811).value.toUpperCase(),
	word3Letter1 = ReactDOM.findDOMNode(this.refs.answer0503).value.toUpperCase(),
	word3Letter2 = ReactDOM.findDOMNode(this.refs.answer0603).value.toUpperCase(),
	word3Letter3 = ReactDOM.findDOMNode(this.refs.answer0703).value.toUpperCase(),
	word3Letter4 = ReactDOM.findDOMNode(this.refs.answer0803).value.toUpperCase(),
	word3Letter5 = ReactDOM.findDOMNode(this.refs.answer0903).value.toUpperCase(),
	word3Letter6 = ReactDOM.findDOMNode(this.refs.answer1003).value.toUpperCase(),
	word3Letter7 = ReactDOM.findDOMNode(this.refs.answer1103).value.toUpperCase(),
	word3Letter8 = ReactDOM.findDOMNode(this.refs.answer1203).value.toUpperCase(),
	word3Letter9 = ReactDOM.findDOMNode(this.refs.answer1303).value.toUpperCase(),
	word4Letter1 = ReactDOM.findDOMNode(this.refs.answer0511).value.toUpperCase(),
	word4Letter2 = ReactDOM.findDOMNode(this.refs.answer0512).value.toUpperCase(),
	word4Letter3 = ReactDOM.findDOMNode(this.refs.answer0513).value.toUpperCase(),
	word4Letter4 = ReactDOM.findDOMNode(this.refs.answer0514).value.toUpperCase(),
	word4Letter5 = ReactDOM.findDOMNode(this.refs.answer0515).value.toUpperCase(),
	word4Letter7 = ReactDOM.findDOMNode(this.refs.answer0517).value.toUpperCase(),
	word4Letter8 = ReactDOM.findDOMNode(this.refs.answer0518).value.toUpperCase(),
	word4Letter9 = ReactDOM.findDOMNode(this.refs.answer0519).value.toUpperCase(),
	word4Letter10 = ReactDOM.findDOMNode(this.refs.answer0520).value.toUpperCase(),
	word5Letter1 = ReactDOM.findDOMNode(this.refs.answer0600).value.toUpperCase(),
	word5Letter2 = ReactDOM.findDOMNode(this.refs.answer0601).value.toUpperCase(),
	word5Letter3 = ReactDOM.findDOMNode(this.refs.answer0602).value.toUpperCase(),
	word5Letter5 = ReactDOM.findDOMNode(this.refs.answer0604).value.toUpperCase(),
	word5Letter6 = ReactDOM.findDOMNode(this.refs.answer0605).value.toUpperCase(),
	word5Letter7 = ReactDOM.findDOMNode(this.refs.answer0606).value.toUpperCase(),
	word5Letter8 = ReactDOM.findDOMNode(this.refs.answer0607).value.toUpperCase(),
	word5Letter9 = ReactDOM.findDOMNode(this.refs.answer0608).value.toUpperCase(),
	word6Letter1 = ReactDOM.findDOMNode(this.refs.answer0809).value.toUpperCase(),
	word6Letter2 = ReactDOM.findDOMNode(this.refs.answer0810).value.toUpperCase(),
	word6Letter4 = ReactDOM.findDOMNode(this.refs.answer0812).value.toUpperCase(),
	word6Letter5 = ReactDOM.findDOMNode(this.refs.answer0813).value.toUpperCase(),
	word6Letter6 = ReactDOM.findDOMNode(this.refs.answer0814).value.toUpperCase(),
	word6Letter7 = ReactDOM.findDOMNode(this.refs.answer0815).value.toUpperCase(),
	word6Letter9 = ReactDOM.findDOMNode(this.refs.answer0817).value.toUpperCase(),
	word6Letter10 = ReactDOM.findDOMNode(this.refs.answer0818).value.toUpperCase(),
	word7Letter1 = ReactDOM.findDOMNode(this.refs.answer1005).value.toUpperCase(),
	word7Letter2 = ReactDOM.findDOMNode(this.refs.answer1006).value.toUpperCase(),
	word7Letter3 = ReactDOM.findDOMNode(this.refs.answer1007).value.toUpperCase(),
	word7Letter4 = ReactDOM.findDOMNode(this.refs.answer1008).value.toUpperCase(),
	word7Letter5 = ReactDOM.findDOMNode(this.refs.answer1009).value.toUpperCase(),
	word7Letter6 = ReactDOM.findDOMNode(this.refs.answer1010).value.toUpperCase(),
	word8Letter1 = ReactDOM.findDOMNode(this.refs.answer1202).value.toUpperCase(),
	word8Letter3 = ReactDOM.findDOMNode(this.refs.answer1204).value.toUpperCase(),
	word8Letter4 = ReactDOM.findDOMNode(this.refs.answer1205).value.toUpperCase(),
	word8Letter5 = ReactDOM.findDOMNode(this.refs.answer1206).value.toUpperCase(),
	word8Letter6 = ReactDOM.findDOMNode(this.refs.answer1207).value.toUpperCase(),
	word8Letter7 = ReactDOM.findDOMNode(this.refs.answer1208).value.toUpperCase(),
	word8Letter8 = ReactDOM.findDOMNode(this.refs.answer1209).value.toUpperCase(),
	word8Letter9 = ReactDOM.findDOMNode(this.refs.answer1210).value.toUpperCase(),
	word9Letter1 = ReactDOM.findDOMNode(this.refs.answer1507).value.toUpperCase(),
	word9Letter2 = ReactDOM.findDOMNode(this.refs.answer1607).value.toUpperCase(),
	word9Letter3 = ReactDOM.findDOMNode(this.refs.answer1707).value.toUpperCase(),
	word9Letter4 = ReactDOM.findDOMNode(this.refs.answer1807).value.toUpperCase(),
	word9Letter5 = ReactDOM.findDOMNode(this.refs.answer1907).value.toUpperCase(),
	word9Letter6 = ReactDOM.findDOMNode(this.refs.answer2007).value.toUpperCase(),
	word10Letter1 = ReactDOM.findDOMNode(this.refs.answer1704).value.toUpperCase(),
	word10Letter2 = ReactDOM.findDOMNode(this.refs.answer1705).value.toUpperCase(),
	word10Letter3 = ReactDOM.findDOMNode(this.refs.answer1706).value.toUpperCase(),
	word10Letter5 = ReactDOM.findDOMNode(this.refs.answer1708).value.toUpperCase(),
	word10Letter6 = ReactDOM.findDOMNode(this.refs.answer1709).value.toUpperCase(),
	word10Letter7 = ReactDOM.findDOMNode(this.refs.answer1710).value.toUpperCase(),
	word10Letter9 = ReactDOM.findDOMNode(this.refs.answer1712).value.toUpperCase(),
	word10Letter10 = ReactDOM.findDOMNode(this.refs.answer1713).value.toUpperCase();

Finally, we check that the letter the user types in is the correct answer to the clues.

if ((word1Letter1 == letter0016) &&	(word1Letter2 == letter0116) &&
   (word1Letter3 == letter0216) && (word1Letter4 == letter0316) &&
   (word1Letter5 == letter0416) && (word1Letter6 == letter0516) &&
   (word1Letter7 == letter0616) && (word1Letter8 == letter0716) &&
   (word1Letter9 == letter0816) && (word2Letter1 == letter0311) && 
   (word2Letter2 == letter0411) && (word2Letter3 == letter0511) && 
   (word2Letter4 == letter0611) && (word2Letter5 == letter0711) && 
   (word2Letter6 == letter0811) && (word2Letter7 == letter0911) && 
   (word2Letter8 == letter1011) && (word2Letter9 == letter1111) && 
   (word2Letter10 == letter1211) && (word2Letter11 == letter1311) && 
   (word2Letter12 == letter1411) && (word2Letter13 == letter1511) &&
   (word2Letter14 == letter1611) && (word2Letter15 == letter1711) &&
   (word2Letter16 == letter1811) && (word3Letter1 == letter0503) && 
   (word3Letter2 == letter0603) && (word3Letter3 == letter0703) && 
   (word3Letter4 == letter0803) && (word3Letter5 == letter0903) && 
   (word3Letter6 == letter1003) && (word3Letter7 == letter1103) && 
   (word3Letter8 == letter1203) && (word3Letter9 == letter1303) &&
   (word4Letter2 == letter0512) && (word4Letter3 == letter0513) &&
   (word4Letter4 == letter0514) && (word4Letter5 == letter0515) &&
   (word4Letter7 == letter0517) && (word4Letter8 == letter0518) &&
   (word4Letter9 == letter0519) && (word4Letter10 == letter0520) &&
   (word5Letter1 == letter0600) && (word5Letter2 == letter0601) &&
   (word5Letter3 == letter0602) && (word5Letter5 == letter0604) &&
   (word5Letter6 == letter0605) && (word5Letter7 == letter0606) &&
   (word5Letter8 == letter0607) && (word5Letter9 == letter0608) &&
   (word6Letter1 == letter0809) && (word6Letter2 == letter0810) &&
   (word6Letter4 == letter0812) && (word6Letter5 == letter0813) &&
   (word6Letter6 == letter0814) && (word6Letter7 == letter0815) &&
   (word6Letter9 == letter0817) && (word6Letter10 == letter0818) &&
   (word7Letter1 == letter1005) && (word7Letter2 == letter1006) &&
   (word7Letter3 == letter1007) && (word7Letter4 == letter1008) &&
   (word7Letter5 == letter1009) && (word7Letter6 == letter1010) &&
   (word8Letter1 == letter1202) && (word8Letter3 == letter1204) &&
   (word8Letter4 == letter1205) && (word8Letter5 == letter1206) &&
   (word8Letter6 == letter1207) && (word8Letter7 == letter1208) &&
   (word8Letter8 == letter1209) && (word8Letter9 == letter1210) &&
   (word9Letter1 == letter1507) && (word9Letter2 == letter1607) &&
   (word9Letter3 == letter1707) && (word9Letter4 == letter1807) &&
   (word9Letter5 == letter1907) && (word9Letter6 == letter2007) &&
   (word10Letter1 == letter1704) &&	(word10Letter2 == letter1705) &&
   (word10Letter3 == letter1706) &&	(word10Letter5 == letter1708) &&
   (word10Letter6 == letter1709) &&	(word10Letter7 == letter1710) &&
   (word10Letter9 == letter1712) &&	(word10Letter10 == letter1713)) {
      alert("CORRECT");
}
else {
      alert("NOT CORRECT");
}

I just demonstrated a basic way of creating a crossword puzzle with React.  There is more that can be done here, like adding dynamic styles if a word is wrong.  What else can you do to improve on this crossword puzzle game?