В этом упражнении показано, как управлять потоком данных, реквизитами и событиями во VueJs. Это простой пост, в котором мы будем применять эти концепции при создании игры в крестики-нолики.

1. Создайте исходный проект Vue.

Чтобы начать новый проект, вам просто нужно запустить

$ vue create game-tic-tac-toe

Чтобы запустить новое приложение

$ npm install
$ npm run serve

Структура файлов в нашем проекте Vue:

node_modules/
public/
   index.html
src/
   assets/
   components/

src / folder - это то место, где будет наш контент. В этом случае мы имеем

src/App.vue

Это наши основные компоненты, все остальные будут от него дочерними.

src/components/HelloWorld.vue

Это пример, созданный при первой установке.

Также у нас есть файл main.js, в котором наш vue монтируется к представлению. Чтобы лучше понять, как это работает https://css-tricks.com/what-does-the-h-stand-for-in-vues-render-method/

2. Создание нашего первого компонента.

Наш первый компонент - это игровой компонент, с которого мы начнем строить нашу игру в крестики-нолики.

$ cd src
$ touch Game.vue

Структура файла Vue такова:

<template>
</template>
<script>
  export default {
    name: 'Game'
  }
</script>
<style>
</style>

Должно быть, работая в нашем первом компоненте.

<template>
  <div class="game-content">
    <h1>Tic Tac Toe</h1>
    <div class="game-board">
      BOARD
    </div>
  </div>
</template>
<script>
  export default {
    name: 'Game'
  }
</script>
<style lang="scss" scoped>
  .game-content{
    text-align: center;
    .game-board{
      width: 400px;
      height: 400px;
      margin: 50px auto;
      background: #cccccc;
    }
  }
</style>

Для управления scss установите:

$ npm install sass-loader node-sass webpack --save-dev

3. Создание доски.

Нам понадобится игровая доска, в данном случае доска 3х3.

Мы собираемся создать этот новый компонент в папке компонентов:

$ cd components/
$ touch Board.vue

Как компонент игры, файловая структура Board Vue имеет следующий вид:

<template>
</template>
<script>
  export default {
    name: 'Board'
  }
</script>
<style>
</style>

Установив 9 мест на нашей доске, мы имеем:

<template>
   <div class="board-content">
       <div class="cell"></div>
       <div class="cell"></div>
       <div class="cell"></div>
       <div class="cell"></div>
       <div class="cell"></div>
       <div class="cell"></div>
       <div class="cell"></div>
       <div class="cell"></div>
       <div class="cell"></div>
   </div>
</template>
<script>
   export default {
       name: 'board'
   }
</script>
<style lang="scss" scoped>
   .board-content{
       height: 100%;
       background: #555555;
       display: grid;
       grid-template-columns: 33.33% 33.33% 33.33%;
       grid-template-rows: 33.33% 33.33% 33.33%;
       .cell{
           align-self: stretch;
           border: #ffffff 3px solid;
       }
   }
</style>

В файл Game.vue мы должны добавить этот новый компонент.

import Board from './components/Board.vue'
export default {
   name: 'Game',
   comments: {
       Board
   }
}

И добавьте тег платы в html-часть.

<template>
  <div class="game-content">
    <h1>Tic Tac Toe</h1>
    <div class="game-board">
      <board></board>
    </div>
  </div>
</template>

На данный момент у нас есть:

4. Создание штучного компонента.

Нам нужен компонент для каждой ячейки. Мы собираемся создать компонент Piece.

$ cd components/
$ touch Piece.vue

Для этого файла его структура:

<template>
</template>
<script>
  export default {
    name: 'Board'
  }
</script>
<style>
</style>

Чтобы отобразить символ, начинающийся только с X, Piece.vue будет выглядеть следующим образом:

<template>
   <div class="piece-content" v-text="symbol"></div>
</template>
<script>
   export default {
       name: 'piece',
       data () {
           return {
               symbol: 'X'
           }
       }
   }
</script>
<style lang="scss" scoped>
   @import url('https://fonts.googleapis.com/css?family=Luckiest+Guy&display=swap');
   $luckiest-guy: 'Luckiest Guy', cursive;
   .piece-content{       
       height: 100%;
       width: 100%;
       font-family: $luckiest-guy;
       color: lime;
       font-size: 100px;
   }
</style>

В Board.vue нужно добавить

import Piece from './Piece.vue'
export default {
   name: 'board',
   components: {
       Piece
   }
}

И в каждую ячейку мы должны включить тег куска

<div class="cell">
   <piece></piece>
</div>

Здесь у нас есть

Но здесь идея состоит в том, чтобы показать символ в частичном компоненте в соответствии с родительским компонентом. Значит, символ должен быть параметром.

Данные от родителя должны управляться с помощью props.

В Piece.vue реквизиты должны быть

<template>
    <div class="piece-content" v-text="symbol"></div>
</template>
<script>
export default {
   name: 'piece',
   props: {
       symbol: {
           type: String,
           default: ''
       }
   }
}
</script>

А в Board.vue вы можете добавить параметр в тег кусок

<piece symbol="X"></piece>

5. Доска общения с фигурами.

Доска должна начинаться пустой, это означает, что фигуры должны быть пустыми. То, что мы знаем, или то, что знает доска, - это позиция каждой фигуры, потому что позиции находятся на доске. Затем мы можем установить значение для каждой позиции в компоненте платы.

Нам нужен элемент, данные, чтобы управлять значением в каждой ячейке (частях).

data () {
   return {
       cells: {
           1: '', 2: '',3: '',4: '',5: '',6: '',7: '',8: '',9: ''
       },
   }
}

И для каждой фигуры в компоненте Board (в соответствии с положением на доске) мы должны установить:

<piece :symbol="cells[1]"></piece>

И в Piece.vue у нас есть

export default {
   name: 'piece',
   props: {
       symbol: {
           type: String,
           default: ''
       }
   }
}

Также важно знать, какие есть варианты для символов, X или O. В этом случае в нашем файле Board.vue у нас есть

data () {
   return {
       cells: {
           1: '', 2: '',3: '',4: '',5: '',6: '',7: '',8: '',9: ''
       },
       symbol: {
           x: 'X',
           o: 'O'
       }
   }
}

Как мы можем узнать, каким должен быть символ, чтобы начать матч? Но когда игра запущена, мы должны знать, какой будет следующий символ. Может быть, мы можем использовать тот же var

data () {
   return {
       cells: {
           1: '', 2: '',3: '',4: '',5: '',6: '',7: '',8: '',9: '',
       },
       symbol: {
           x: 'X',
           o: 'O'
       },
       nextSymbol: 'x'
   }
}

6. Излучающие события

Как управлять событиями. Событие установки символа в пробел должно быть в компоненте Piece. Затем мы можем использовать событие щелчка.

<div class="piece-content" @click="tapHandler" v-text="symbol"></div>

И мы должны создать метод и испустить событие, чтобы компонент Board знал, что происходит.

methods: {
   clickHandler() {
       this.$emit("tap");
   }
}

В компоненте Board мы получаем событие касания

<piece :symbol="cells[1]" @tap="onTap(1)"></piece>
<piece :symbol="cells[2]" @tap="onTap(2)"></piece>
<piece :symbol="cells[3]" @tap="onTap(3)"></piece>
<piece :symbol="cells[4]" @tap="onTap(4)"></piece>
<piece :symbol="cells[5]" @tap="onTap(5)"></piece>
<piece :symbol="cells[6]" @tap="onTap(6)"></piece>
<piece :symbol="cells[7]" @tap="onTap(7)"></piece>
<piece :symbol="cells[8]" @tap="onTap(8)"></piece>
<piece :symbol="cells[9]" @tap="onTap(9)"></piece>

И метод должен быть

methods: {
   onTap (position) {
       this.cells[position] = this.symbol[this.nextSymbol]
       this.updateNextSymbol()
   }
}

Реализация updateNextSymbol

methods: {
   onTap (position) {
       this.cells[position] = this.symbol[this.nextSymbol]
       this.updateNextSymbol()
   },
   updateNextSymbol () {
       if ( this.nextSymbol == 'x' ) {
           this.nextSymbol = 'o'
       } else {
           this.nextSymbol = 'x'
       }
   }
}

7. Излучающие события

Мы должны управлять правилами этой игры. Правила для победителя завершаются, когда один и тот же символ находится в трех частях в ряду. Возможны следующие варианты:

Нам нужны некоторые данные в данных игры, чтобы управлять ими.

data () {
    return {
        winner: '',
        playing: true,
        rules: [
              [1,2,3]
            , [1,4,7]
            , [1,5,9]
            , [2,5,8]
            , [3,5,7]
            , [3,6,9]
            , [4,5,6]
            , [7,8,9]
        ],
    }
},

Кроме того, нам нужно получать привязку Board каждый раз, когда происходит изменение. В этом случае необходимо получить событие обновления и управлять им в компоненте Game.

В Board.vue

onTap (position) {
    if (this.cells[position] == ''){
        this.cells[position] = this.symbol[this.nextSymbol]
        this.updateNextSymbol()
        this.$emit("update", {
            cells: this.cells,
            symbol: this.cells[position]
        })
    }
}

В Game.vue

<board @update="updateStatus"></board>
.
.
.
methods: {
    updateStatus (snap) {
        var self = this
        this.rules.some(function(rule) {
            var status = true;
            rule.every(function(position) {
                if (snap.cells[position] != snap.symbol) {
                    status = false;
                }
                return status;
            });
            if (status) {
                self.winner = snap.symbol;
                self.playing = false
            }
            return status;
        });
    }
}

7. Управление победой

Если есть победитель, мы должны показать это сообщение. В этом случае шаблон в Games.vue может быть таким:

<template>
    <div class="game-content">
        <h1>Tic Tac Toe</h1>
        <div class="game-board" v-if="playing">
            <board @update="updateStatus"></board>
        </div>
        <div v-else>
            <h3>The winner is <span v-text="winner"></span></h3>
            <button value="Play" @click="play">Play</button>
        </div>
    </div>
</template>

И методы должны быть

methods: {
    updateStatus (snap) {
        var self = this
        this.rules.some(function(rule) {
            var status = true;
            rule.every(function(position) {
                if (snap.cells[position] != snap.symbol) {
                    status = false;
                }
                return status;
            });
            if (status) {
                self.winner = snap.symbol;
                self.playing = false
            }
            return status;
        });
    },
    play () {
        this.playing = true
    }
}

Мы можем добавить другие стили

<style lang="scss" scoped>
    @import url('https://fonts.googleapis.com/css?family=Baloo+Da&display=swap');
    $baloo-da: 'Baloo Da', cursive;

    h1{
        font-family: $baloo-da;
    }

    h3{
        font-family: $baloo-da;
        font-size: 80px;

        span{
            color: red;
        }
    }

    .game-content{
        text-align: center;
        .game-board{
            width: 400px;
            height: 400px;
            margin: 20px auto;
            background: #cccccc;
        }
    }
</style>

Когда есть победитель, он должен показать

8. Дополнительное задание

Если мы хотим настроить символы для использования в качестве частей, нам просто нужно сделать:

Добавьте эти строки в наш файл Board.vue

props: {
    x: {
        type: String,
        default: 'X'
    },
    o: {
        type: String,
        default: 'O'
    }
},
mounted () {
    this.symbol.x = this.x
    this.symbol.o = this.o
}

А в компоненте Game просто установите наши собственные символы.

<board @update="updateStatus" x="+" o="-"></board>

Здесь, когда мы играем ...

9. Запустите этот код.

Https://github.com/aosmorac/game-tic-tac-toe