Vue 3

Документация

https://vuejs.org/guide/introduction.html 

Установка Vue

npm install -g @vue/cli // устанавливаем vue cli глобально

vue create my-project // создаём проект

npm run serve // запускаем проект

Корневые директории, с которыми будем работать

- public

- src

В public лежит файл index.html, в нём div с id="root".

Сюда будет монтироваться приложение

В src файл main.js в который импортируется компонент App.vue и передаётся (монтируется) в index.html

import { createApp } from 'vue'
import App from './App'
createApp(App).mount('#app')

Компоненты

Каждый компонент это объект с набором полей.

В поле data приходят данные, которые будет использовать компонент, в поле methods пишем функции.

Чтобы создать компонент, создаём файл с его названием и расширением .vue.

Для названия созданных нами компонентов обязательно использовать СamelCase, этим они отличаются, от компонентов, созданных самим фреймворком, например, компонента App 

Затем заходим в файл компонента, пишем vbase, затем нажимаем Tab и перед нами базовая структура компонента - template, script, style.

Sass

Компонент style, который создаётся командой vbase содержит параметры lang="scss" и scoped

Последний означает, что стили будут применяться только к данному конкретному компоненту

С lang="scss" приложение возвращает ошибку: "Can't resolve 'sass-loader'"

Чтобы разрешить использование scss стилей во vue, устанавливаем два пакета sass-loader и node-sass:

npm install sass-loader -D

npm install node-sass -D

Или в процессе установки vue нужно было разрешить использовать css-препроцессор

Счётчик лайков

<template>
  <div>
    <button v-on:click="addLike">Like</button>
    <button @click="addDislike">Dislike</button>
  </div>
  <div>
    Likes count - <strong>{{ likes }}</strong>
  </div>
  <div>
    Disikes count - <strong>{{ dislikes }}</strong>
  </div>
</template>

<script>
export default {
  data() {
    return {
      likes: 0,
      dislikes: 0
    }
  },
  methods: {
    addLike() {
      this.likes += 1;
    },
    addDislike() {
      this.dislikes += 1;
    },
  }
}
</script>

 {{ likes }} - это интерполяция с её помощью данные из data передаём в template

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

<img v-bind:src=post.image />

или короткая запись:

<img :src=post.image />

v-on:click="addLike" - так во Vue записываем addEventListener "click"

@click="addDislike" - это короткая запись аналогичная предыдущей

this.likes - если в методе используем данные из data, пишем не data.likes, а this.likes

Список постов

<template>
  <div>
    <div class="post" v-for="post in posts" :key='post.id'>
      <div><strong>Title:</strong> {{post.title}}</div>
      <div><strong>Description:</strong> {{ post.body }}</div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      posts: [
        { id: 1, title: 'JavaScript1', body: 'JavaScript1 - modern language' },
        { id: 2, title: 'JavaScript2', body: 'JavaScript2 - modern language' },
        { id: 3, title: 'JavaScript3', body: 'JavaScript3 - modern language' },
        { id: 4, title: 'JavaScript4', body: 'JavaScript4 - modern language' },
      ]
    }
  },

В data помещаем данные постов, а в template выводим их на страницу

Запись v-for="post in posts" в родительском элементе позволяет итерироваться по массиву постов

Создание поста

Создаём форму с двумя инпутами и кнопкой отправить

    <form @submit.prevent>
      <input v-bind:value="title" @input="title = $event.target.value" type="text" placeholder="Post title">
      <input v-bind:value="body" @input="body = $event.target.value" type="text" placeholder="Post body">
      <button @click="createPost">Add post</button>
    </form>

@submit.prevent - отключаем для формы событие по умолчанию (отправка формы с перезагрузкой страницы)

v-bind:value="title"- привязываем данный инпут к полю title объекта data данного модуля

@input="title = $event.target.value" - указываем, что title равно тексту, который в данный инпут вводится

@click="createPost" - при клике по кнопке выполняем функцию createPost

Саму функцию createPost записываем в объекте methods

methods: {
    createPost() {
      const newPost = {
        id: Date.now(),
        title: this.title,
        body: this.body
      }
      this.posts.push(newPost)
    }
  }

Разбиваем код на модули

в папке src/components создаём файлы PostList.vue и PostForm.vue
Переносим в них разметку и стили 
Переходим в файл App.vue и в поле script импортируем и регистрируем модули

<script>
import PostList from '@/components/PostList.vue'
import PostForm from '@/components/PostForm.vue'
export default {
  components: {
    PostList, PostForm
  }, 
}
</script>

Затем их можно добавить в разметку 

<template>
  <div>
    <PostForm/>
    <PostList/>    
  </div>
</template>

Форма отобразилась, а список постов нет, так как он не получил данные data, которые остались в App.vue

Пропсы

Список постов передаём в виде пропса в файл PostList.vue

<script>
export default {
  props: {
    posts: {
      type: Array,
      required: true
    }
  }
}
</script>

В файле App.vue передаём пропс

<PostList v-bind:posts="posts"/>  

или короткая запись (по аналогии как v-on:click можно заменить @click)

<PostList :posts="posts"/>  


Ещё один важный момент.

Пропсы в дочернем компоненте изменять нельзя!

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


Передача данных от ребёнка родителю

Пропсы позволяют передать данные от родителя - компонента App.vue к ребёнку - компоненту PostList.vue.

Благодаря пропсам список постов у нас отобразился.

Но возможности добавлять новые посты нет, так как она была заложена в форме, которую мы вынесли в отдельный компонент.

Нужно научиться передавать данные от дочернего компонента PostForm.vue к родителю App.vue.

В компонент PostForm.vue добавляем объект data:

<script>
  export default {
    data() {
      return {
        post: {
          title: '',
          body: '',
        }
      }
    }
  }
</script>

в инпутах меняем привязку и направление передачи данных с title и body на post.title и post.body, т.е передаём данные в созданный объект data:

<input v-bind:value="post.title" @input="post.title = $event.target.value" type="text" placeholder="Post title">
<input v-bind:value="post.body" @input="post.body = $event.target.value" type="text" placeholder="Post body">

Такой подход: привязать данные к полю, передать данные в поле - корректный, но не оптимальный. 
Для работы с инпутами, чекбоксами, списками во вью используется директива v-model.

Нам достаточно указать директиву  v-model и указать с какой моделью мы делаем двустороннее связывание

Запись 
v-bind:value="post.title" @input="post.title = $event.target.value"
заменяем на 
v-model="post.title"

Инпуты теперь выглядят так:

<input v-model="post.title" type="text" placeholder="Post title">
<input v-model="post.body" type="text" placeholder="Post body">

Чтобы передать данные от ребёнка к родителю, ребёнок генерирует событие, в нашем случае create(post), а родитель подписывается на это событие @create = "createPost", точно так же, как он подписался бы на события клик, инпут, чендж и т д.

Метод в форме PostForm.vue записывается так
methods: {
      createPost() {
        this.post.id = Date.now();
        this.$emit("create", this.post);
        this.post = {
          title: '',
          body: '',
        }        
      }
    }

Директива this.$emit("create", this.posts); - эмитер события. она генерирует событие, которое передаётся родителю. Первый аргумент - название события, его мы придумываем сами, второй аргумент - что передаём от дочернего элемента в родительский, в нашем случае post. 

Переходим в родительский элемент App.vue, чтобы на это событие подписаться

<template>
  <div>
    <PostForm @create="createPost" />
    <PostList :posts="posts" />    
  </div>
</template>

Запись @create="createPost" - это подписка на событие

Здесь @create - название события, на которое мы подписались (то. которое придумали и указали в эмитере), "createPost" - какая функция будет вызываться, когда это событие произойдёт.

Функцию прописываем в методе App.vue

methods: {
    createPost(post) {
      this.posts.push(post)
    }
  }

Функция принимает один аргумент post - тот, который передал ей эмитер, и добавляет этот пост в массив постов.

Выносим пост в отдельный компонент

Хорошие компоненты - независимые компоненты

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

Сейчас форма принимает данные про пост и передаёт их, список постов принимает данные и отрисовывает посты. Создадим отдельный компонент для отдельного поста.

в папке components создаём компонент PostItem.vue, переносим в него разметку и стили из PostList.vue, а также добавляем пропс

<template>
  <div class="post">
    <div><strong>Title:</strong> {{ post.title }}</div>
    <div><strong>Description:</strong> {{ post.body }}</div>
  </div>
</template>

<script>
export default {
  props: {
    post: {
      type: Object,
      required: true
    }
  }
}
</script>

В компоненте PostList.vue импортируем и регистрируем компонент PostItem.vue и добавляем его в разметку:

<template>
  <div>
    <PostItem v-for="post in posts" :post="post" />
  </div>
</template>

<script>
import PostItem from '@/components/PostItem';
export default {
  components: {PostItem},
  props: {
    posts: {
      type: Array,
      required: true
    }
  }
}
</script>


https://youtu.be/XzLuMtDelGk?t=2948 - удаление поста из списка

https://youtu.be/XzLuMtDelGk?t=3012 - UI компоненты

https://youtu.be/XzLuMtDelGk?t=4048 - условная отрисовка

Для условной отрисовки есть две директивы

v-if="posts.length > 0"... v-else

v-show="length > 0", v-show="length < 0"

Первая полностью удаляет блок свойством display: none

Вторая отображает или скрывает его свойством visibility: visible/hidden

https://youtu.be/XzLuMtDelGk?t=4220 - модальное окно

https://youtu.be/XzLuMtDelGk?t=4560 = модификаторы v-model

v-model.trim - удаляет пробелы в начале и в конце

v-model.number - от инпута получаем число, а не строку

v-model.lazy - позволяет отслеживать событие on-change вместо input

Работа с сервером

Данные получаем от https://jsonplaceholder.typicode.com/ по запросу https://jsonplaceholder.typicode.com/posts?_limit=10

Устанавливаем axios
npm i axios

Импортируем в поле script App.vue
import axios from 'axios';

В поле methods пишем функцию fetchPosts
async fetchPosts() {
    try {
      const response = await axios.get('https://jsonplaceholder.typicode.com/posts?_limit=10');
      this.posts = response.data;
    } catch(e) {
      console.log(error)
    }
  }

В template добавляем кнопку при клике по которой отображается список постов
<button @click="fetchPosts">Get posts</button>

Чтобы посты подгружались без кликов по кнопке, вызываем функцию fetchPosts в жизненном цикле mounted
mounted() {
  this.fetchPosts();
}

Composition API
Скрипт записываем как <script setup> b и внутри пишем обычный js код

Реактивность - два способа создания



- директива v-show или v-if