Skip to content

Instantly share code, notes, and snippets.

@Ravenstine
Last active August 5, 2020 01:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Ravenstine/8fa62cb81a790a3afb6713fd9f2480b5 to your computer and use it in GitHub Desktop.
Save Ravenstine/8fa62cb81a790a3afb6713fd9f2480b5 to your computer and use it in GitHub Desktop.
Ember Custom Elements: Tic Tac Toe w/ React Demo
{{#each this.rows as |row|}}
<div class="board-row">
{{#each row as |square|}}
<button class="square" {{on "click" (fn this.selectSquare square.index)}}>
{{square.value}}
</button>
{{/each}}
</div>
{{/each}}
import Component from '@glimmer/component';
import { customElement } from 'ember-custom-elements';
import { action } from '@ember/object';
@customElement('game-board')
export default class GameBoardComponent extends Component {
get squares() {
return JSON.parse(this.args.squares).map((square, i) => {
return { value: square, index: i };
});
}
get rows() {
return this.squares.reduce((rows, square) => {
let row = rows[rows.length - 1];
if (row.length === 3) {
row = [];
rows.push(row);
}
row.push(square);
return rows;
}, [[]]);
}
@action
selectSquare(i, { target }) {
const event = new CustomEvent('squareselected', {
bubbles: true,
detail: { i }
});
target.dispatchEvent(event);
}
}
<div>{{@status}}</div>
<ol>
{{#each this.history as |step move|}}
<li>
<button {{on "click" (fn this.selectMove move)}}>
{{#if move}}
Go to move #{{move}}
{{else}}
Go to game start
{{/if}}
</button>
</li>
{{/each}}
</ol>
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import {
customElement,
forwarded,
getCustomElement
} from 'ember-custom-elements';
@customElement('game-history')
export default class GameHistoryComponent extends Component {
@forwarded
@tracked
history;
@action
selectMove(move) {
const event = new CustomEvent('moveselected', {
bubbles: true,
detail: { move }
});
getCustomElement(this).dispatchEvent(event);
}
}
import Controller from '@ember/controller';
import bootGame from '../lib/tic-tac-toe';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class ApplicationController extends Controller {
@action
didInsertGame(element) {
bootGame(element);
}
}
function calculateWinner(squares) {
const lines = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]];
for (const [a, b, c] of lines) {
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill('')
}],
stepNumber: 0,
xIsNext: true
};
this.selectSquare = this.selectSquare.bind(this);
this.jumpTo = this.jumpTo.bind(this);
}
componentDidMount() {
const node = ReactDOM.findDOMNode(this);
node.addEventListener('squareselected', this.selectSquare);
node.addEventListener('moveselected', this.jumpTo);
this.updateGameHistory();
}
componentWillUnmount() {
const node = ReactDOM.findDOMNode(this);
node.removeEventListener('squareselected', this.selectSquare);
node.removeEventListener('moveselected', this.jumpTo);
}
updateGameHistory() {
const node = ReactDOM.findDOMNode(this);
const history = node.querySelector('game-history');
history.history = this.state.history;
}
selectSquare({ detail: { i } }) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares
}]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext
});
this.updateGameHistory();
}
jumpTo({ detail: { move } }) {
this.setState({
stepNumber: move,
xIsNext: move % 2 === 0
});
}
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return React.createElement('div', {
className: 'game'
}, React.createElement('game-board', {
squares: JSON.stringify(current.squares)
}), React.createElement('game-history', {
status
}));
}
}
export default function bootGame(element) {
ReactDOM.render(React.createElement(Game), element);
}
body {
font: 14px "Century Gothic", Futura, sans-serif;
margin: 20px;
}
ol, ul {
padding-left: 30px;
}
.board-row:after {
clear: both;
content: "";
display: table;
}
.status {
margin-bottom: 10px;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.square:focus {
outline: none;
}
.kbd-navigation .square:focus {
background: #ddd;
}
.game {
display: flex;
flex-direction: row;
}
game-history, .game-info {
margin-left: 20px;
}
<div id="game" {{did-insert this.didInsertGame}}>
</div>
{
"version": "0.17.1",
"EmberENV": {
"FEATURES": {},
"_TEMPLATE_ONLY_GLIMMER_COMPONENTS": false,
"_APPLICATION_TEMPLATE_WRAPPER": true,
"_JQUERY_INTEGRATION": false
},
"ENV": {
"emberCustomElements": {
"deoptimizeModuleEval": true,
"defaultOptions": {
"useShadowRoot": false
}
}
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"ember": "3.18.1",
"ember-template-compiler": "3.18.1",
"ember-testing": "3.18.1",
"react": "https://unpkg.com/react@16/umd/react.development.js",
"react-dom": "https://unpkg.com/react-dom@16/umd/react-dom.development.js"
},
"addons": {
"@glimmer/component": "1.0.0",
"ember-custom-elements": "2.0.0",
"@ember/render-modifiers": "1.0.2"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment