Основы компонентов Vue.js: от создания до динамических форм

Основы компонентов Vue.js: от создания до динамических форм

Картинка к публикации: Основы компонентов Vue.js: от создания до динамических форм

Основы компонентов

Создание компонента

В тиши своей комнаты, наш разработчик взялся за перо и решил описать процесс создания компонента в Vue3. Начало этому было положено с понимания того, что каждый компонент в Vue3 является сущностью, которая объединяет шаблон, скрипт и стили в единое целое.

Первым шагом на этом пути стало создание файла компонента. Пусть это будет файл MyComponent.vue, который удобно разместить в директории src/components. В нем будет три секции: <template>, <script> и <style>, каждая из которых выполняет свою уникальную функцию.

<template>
  <div class="my-component">
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>
  </div>
</template>

<script>
export default {
  name: 'MyComponent',
  data() {
    return {
      title: 'Привет, мир!',
      description: 'Это мой первый компонент на Vue3.'
    };
  }
};
</script>

<style scoped>
.my-component {
  text-align: center;
}

.my-component h1 {
  color: #42b983;
}
</style>

Шаблон компонента (<template>) отвечает за структуру и отображение данных. В данном случае мы видим простой HTML-код с заголовком и абзацем. Эти элементы связаны с данными через двойные фигурные скобки {{ }}, которые позволяют выводить значения переменных.

Следующая секция – это скрипт (<script>). Здесь определяется логика компонента. Мы экспортируем объект по умолчанию с именем компонента name и функцией data, которая возвращает объект данных для использования в шаблоне. В нашем примере эти данные включают заголовок title и описание description.

Последняя секция – стили (<style>). Она предназначена для оформления компонентов. Ключевое слово scoped гарантирует, что стили будут применяться только к данному компоненту, исключая возможность конфликта с другими стилями на странице.

Завершив создание компонента, необходимо зарегистрировать его в основном приложении или другом родительском компоненте. Для этого откроем файл App.vue (или любой другой родительский компонент) и импортируем наш новый компонент:

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

<script>
import MyComponent from './components/MyComponent.vue';

export default {
  name: 'App',
  components: {
    MyComponent
  }
};
</script>

<style>
/* Ваши глобальные стили */
</style>

Теперь наш компонент готов к использованию! В результате простых действий мы создали инструмент для построения интерфейсов – базовый Vue-компонент, который легко масштабируется и переиспользуется. Как будто сам Толстой взялся бы за кисть программирования, каждый штрих здесь исполнен с точностью и вниманием к деталям, создавая фундамент для великих свершений в мире веб-разработки.

Жизненный цикл компонента

Жизненный цикл компонента в Vue3, как и жизнь человека, состоит из нескольких этапов: рождение, взросление и неизбежный конец. Каждый компонент проходит через эти стадии, начиная от создания и заканчивая уничтожением.

Первый шаг на пути любого компонента – это его создание. Этот процесс начинается с вызова метода beforeCreate(), когда экземпляр компонента только создается, но еще не готов к использованию. Словно младенец в утробе матери, компонент еще не видит света:

export default {
  name: 'MyComponent',
  beforeCreate() {
    console.log('Компонент создается');
  }
};

Следующим этапом является метод created(). Здесь компонент уже готов для работы с данными и реактивностью. Это словно ребенок, который впервые открывает глаза на мир:

export default {
  name: 'MyComponent',
  created() {
    console.log('Компонент создан');
  }
};

Переходя к фазе монтирования компонента в DOM-дерево, вызываются методы beforeMount() и mounted(). В этот момент наш виртуальный герой выходит на сцену браузера:

export default {
  name: 'MyComponent',
  beforeMount() {
    console.log('Компонент скоро будет смонтирован');
  },
  mounted() {
    console.log('Компонент смонтирован');
  }
};

Когда данные или пропсы изменяются, начинается процесс обновления компонента. Здесь вступают в игру методы beforeUpdate() и updated(), которые позволяют следить за изменениями состояния:

export default {
  name: 'MyComponent',
  beforeUpdate() {
    console.log('Компонент собирается обновиться');
  },
  updated() {
    console.log('Компонент обновлен');
  }
};

И наконец, как всякая история имеет свой конец, так и у компонентов есть свои завершающие моменты. Методы beforeUnmount() и unmounted() сигнализируют о том, что компонент покидает сцену навсегда:

export default {
  name: 'MyComponent',
  beforeUnmount() {
    console.log('Компонент скоро будет размонтирован');
  },
  unmounted() {
    console.log('Компонент размонтирован');
  }
};

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

<template>
  <div class="lifecycle-component">
    <h2>Жизненный цикл компонента</h2>
    <p>{{ message }}</p>
    <button @click="updateMessage">Обновить сообщение</button>
  </div>
</template>

<script>
export default {
  name: 'LifecycleComponent',
  
  data(){
    return{
      message:'Привет!'
    };
  },    
  methods:{
  updateMessage(){
      this.message='Сообщение обновлено';
    }
  },
  beforeCreate(){
    console.log('beforeCreate');
  },
  created(){
    console.log('created');
  },
  beforeMount(){
    console.log('beforeMount');
  },
  mounted(){
    console.log('mounted');
  },
  beforeUpdate(){
    console.log('beforeUpdate');
  },
  updated(){
    console.log('updated');
  },
  beforeUnmount(){
    console.log('beforeUnmount');
  },
  unmounted(){
    console.log('unmounted');
  }
};
</script>

<style scoped>
  .lifecycle-component{
    text-align:center;
  }
</style>

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

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

Как яркие главы увлекательного романа, каждый компонент в Vue3 несет свою уникальную роль и значение. В этом разделе мы создадим три ключевых компонента: header, body и footer, которые будут служить основой для дальнейших примеров. Они подобны актерам на сцене, каждый из которых исполняет свои реплики и действия, формируя целостное повествование нашего приложения.

Начнем с создания компонента Header. Этот компонент будет отображать заголовок страницы и навигационное меню:

<template>
  <header class="app-header">
    <h1>{{ title }}</h1>
    <nav>
      <ul>
        <li><a href="#">Главная</a></li>
        <li><a href="#">О нас</a></li>
        <li><a href="#">Контакты</a></li>
      </ul>
    </nav>
  </header>
</template>

<script>
export default {
  name: 'AppHeader',
  data() {
    return {
      title: 'Мое Приложение'
    };
  }
};
</script>

<style scoped>
.app-header {
  background-color: #42b983;
  color: white;
  padding: 20px;
}
.app-header nav ul {
  list-style-type: none;
  padding: 0;
}
.app-header nav ul li {
  display: inline;
  margin-right: 10px;
}
</style>

Теперь перейдем к созданию компонента Body. Этот компонент будет содержать основной контент страницы:

<template>
  <main class="app-body">
    <slot></slot> <!-- Используем слот для вставки динамического контента -->
  </main>
</template>

<script>
export default {
  name: 'AppBody'
};
</script>

<style scoped>
.app-body {
  flex: 1;
  padding: 20px;
}
</style>

И наконец, завершим наш ансамбль компонентом Footer, который будет отображать нижнюю часть страницы с контактной информацией или авторскими правами:

<template>
  <footer class="app-footer">
    <p>&copy; {{ currentYear }} Мое Приложение. Все права защищены.</p>
  </footer>
</template>

<script>
export default {
  name: 'AppFooter',  
  data(){
    return{
      currentYear:new Date().getFullYear()
  };}
};
</script>

<style scoped>
.app-footer {
  background-color: #333;
  color: white;
  text-align: center;
  padding: 10px;
}
</style>

Объединим все эти компоненты в главном приложении App.vue:

<template> 
  <div id="app"> 
    <AppHeader /> 
    <AppBody> 
      <h2>Добро пожаловать!</h2> 
      <p>Здесь вы найдете интересную информацию.</p> 
    </AppBody> 
    <AppFooter /> 
  </div> 
</template>

<script> 
import AppHeader from './components/AppHeader.vue';
import AppBody from './components/AppBody.vue';
import AppFooter from './components/AppFooter.vue';

export default {
  name:'App',
  components:{
    AppHeader,
    AppBody,
    AppFooter
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
html, body {
  height: 100%;
  margin: 0;
}
</style>

Эти три компонента – словно главы одного произведения – теперь работают вместе, чтобы создать гармоничную структуру нашего приложения. В последующих разделах мы будем детально разбирать их работу и взаимодействие, углубляясь в таинства Vue3 и раскрывая перед вами новые горизонты веб-разработки.

Коммуникация между компонентами

Передача данных через Props

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

Начнем с основного сценария: родительский компонент передает данные дочернему компоненту. Представьте себе сцену из пьесы, где главный герой делится важной информацией со своим спутником. Так же и в Vue3 родительский компонент может "рассказать" своему дочернему компоненту что-то значимое.

Рассмотрим пример. Пусть у нас есть родительский компонент App.vue:

<template>
  <div id="app">
    <AppHeader :title="appTitle" />
    <AppBody>
      <h2>Добро пожаловать!</h2>
      <p>Здесь вы найдете интересную информацию.</p>
    </AppBody>
    <AppFooter />
  </div>
</template>

<script>
import AppHeader from './components/AppHeader.vue';
import AppBody from './components/AppBody.vue';
import AppFooter from './components/AppFooter.vue';

export default {
  name: 'App',
  components: {
    AppHeader,
    AppBody,
    AppFooter
  },
  data() {
    return {
      appTitle: 'Мое Лучшее Приложение'
    };
  }
};

<style>
/* Ваши глобальные стили */
</style>

В данном примере appTitle является данными, которые мы хотим передать дочернему компоненту AppHeader. Чтобы это сделать, мы используем директиву v-bind (или сокращенно :), которая связывает значение свойства appTitle с prop title компонента AppHeader.

Теперь давайте обновим наш компонент AppHeader, чтобы он принимал этот проп:

<template>
  <header class="app-header">
    <h1>{{ title }}</h1>
    <nav>
      <ul>
        <li><a href="#">Главная</a></li>
        <li><a href="#">О нас</a></li>
        <li><a href="#">Контакты</a></li>
      </ul>
    </nav>
  </header>
</template>

<script>
// Включаем props для компонента
export default {
  name: 'AppHeader',
  props: {
    title: String // Типизация пропа
  }
};
</script>

<style scoped>
/* Ваши стили */
</style> 

В этом коде ключевой элемент – объект props. Мы определяем проп под именем title и указываем его тип как String. Это не только помогает нам понять структуру данных, но также позволяет Vue выполнять базовую проверку типов.

Для более сложных случаев можно использовать объектную форму props для добавления дополнительных проверок и обязательных полей:

props: {
  title:{
    type:String,
    required:true,
    default:'Заголовок по умолчанию'
  }
}

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

Пропсы могут принимать не только примитивные типы данных (строки, числа), но также объекты и массивы. Рассмотрим пример с передачей массива ссылок навигационного меню:

<template> 
  <header class="app-header"> 
    <h1>{{ title }}</h1> 
    <nav> 
      <ul> 
        <li v-for="link in links" :key="link.text"> 
          <a :href="link.href">{{ link.text }}</a> 
        </li> 
      </ul> 
    </nav> 
  </header> 
</template>

<script>
export default{
  name:'AppHeader',
  props:{
    title:String,
    links:Array
  },
};
</script>

<style scoped>
/* Ваши стили */
</style>

Таким образом, создается механизм передачи данных от родителя к потомку, подобно тому как автор романа раскрывает одну тайну за другой на страницах своей книги. Использование Props способствует созданию чистого кода и гибкой архитектуры приложения.

Пользовательские события

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

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

Начнем с простого примера. Пусть у нас есть форма ввода данных в дочернем компоненте AppForm.vue, которая должна оповестить родительский компонент о том, что пользователь нажал кнопку "Отправить":

<template>
  <div class="app-send-form">
    <input v-model="inputValue" type="text" placeholder="Введите текст" />
    <button @click="sendData">Отправить</button>
  </div>
</template>

<script>
export default {
  name: 'AppForm',
  data() {
    return {
      inputValue: ''
    };
  },
  methods: {
    sendData() {
      this.$emit('formSubmitted', this.inputValue);
    }
  }
};
</script>

<style scoped>
.app-form {
  margin: 20px;
}
.app-form input {
  padding: 10px;
}
.app-form button {
  padding: 10px;
}
</style>

В этом примере используется метод $emit для создания пользовательского события formSubmitted, которое несет данные из поля ввода inputValue. Это событие будет прослушиваться родительским компонентом.

Теперь давайте обновим наш родительский компонент App.vue, чтобы он мог обработать это событие:

<template>
  <div id="app">
    <AppHeader :title="appTitle" />
    <AppBody>
      <h2>Добро пожаловать!</h2>
      <p>Здесь вы найдете интересную информацию.</p>
      <!-- Добавляем наш AppForm -->
      <AppForm @formSubmitted="handleFormSubmission" />
    </AppBody>
    <AppFooter />
  </div>
</template>

<script>
import AppHeader from './components/AppHeader.vue';
import AppBody from './components/AppBody.vue';
import AppFooter from './components/AppFooter.vue';
import AppForm from './components/AppForm.vue';

export default {
  name: 'App',
  components: {
    AppHeader,
    AppBody,
    AppFooter,
    AppForm
  },
  data() {
    return {
      appTitle: 'Мое Приложение'
    };
  },
  methods:{
    handleFormSubmission(data){
      console.log('Данные формы:',data);    
    }
  }
};
</script>

<style>. /* Ваши глобальные стили */ </style>

В данном случае мы используем директиву @ (или v-on) для прослушивания события formSubmitted и связываем его с методом handleFormSubmission. Когда событие произойдет, этот метод получит данные от дочернего компонента и выполнит необходимую логику.

Сложные приложения часто требуют передачи более специфичной информации между компонентами. Рассмотрим пример с передачей объекта:

<template>
  <div class="app-user">
    <p>{{ user.name }}</p>
    <button @click="selectUser">Выбрать пользователя</button>
  </div>
</template>

<script>
export default {
  name: 'UserComponent',
  props: {
    user: Object
  },
  methods: {
    selectUser() {
      this.$emit('userSelected', this.user);
    }
  }
};
</script>

<style scoped>
.app-user {
  margin-bottom: 10px;
}
</style>


<!-- Родительский компонент -->

<template>
  <div id="app">
    <UserComponent
      v-for="user in users"
      :key="user.id"
      :user="user"
      @userSelected="handleUserSelection"
    />
  </div>
</template>

<script>
import UserComponent from './components/UserComponent.vue';

export default {
  name: 'App',
  components: {
    UserComponent
  },
  data() {
    return {
      users: [
        { id: 1, name: 'Александр' },
        { id: 2, name: 'Мария' }
      ]
    };
  },
  methods: {
    handleUserSelection(user) {
      console.log('Пользователь выбран:', user);
    }
  }
};
</script>

Таким образом, использование метода `$emit` позволяет нам построить сложные цепочки взаимодействий между компонентами, подобно тому как мастер слова строит диалоги своих героев. Компоненты становятся не просто элементами интерфейса, а полноценными участниками событийного повествования нашего приложения.

Использование ref и emit

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

Начнем с понимания механизма ref. В Vue3 директива ref позволяет нам ссылаться на конкретный элемент или компонент внутри нашего шаблона, предоставляя прямой доступ к нему из методов или других частей логики компонента. Это похоже на то, как писатель держит в руках ключевые заметки о своих персонажах, чтобы придать повествованию глубину и связность.

Рассмотрим пример использования ref. Пусть у нас есть простой дочерний компонент с формой ввода текста:

<template>
  <div class="child-component">
    <input ref="textInput" type="text" placeholder="Введите текст" />
    <button @click="clearInput">Очистить</button>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  methods: {
    clearInput() {
      this.$refs.textInput.value = '';
    }
  }
};
</script>

<style scoped>
.child-component {
  margin: 20px;
}
.child-component input {
  padding: 10px;
}
.child-component button {
  padding: 10px;
}
</style>

В данном примере мы используем директиву ref, чтобы создать ссылку на элемент <input>, которую затем можно использовать в методе clearInput. Этот метод очищает значение поля ввода при нажатии кнопки "Очистить".

Теперь добавим родительский компонент, который будет взаимодействовать с этим дочерним компонентом через ref:

<template>
  <div id="app">
    <ChildComponent ref="childComp" />
    <button @click="focusOnChildInput">Фокус на поле ввода</button>
  </div>
</template>

<script>
import ChildComponent from './components/ChildComponent.vue';

export default {
  name: 'App',
  components: {
    ChildComponent
  },
methods:{
focusOnChildInput(){
this.$refs.childComp.$refs.textInput.focus();}
}
};
</script>

<style> /* Ваши глобальные стили */ </style> 

Здесь мы используем директиву ref для ссылки на весь дочерний компонент (ChildComponent). Затем в методе focusOnChildInput получаем доступ к полю ввода внутри этого компонента и устанавливаем фокус на нем.

Теперь перейдем к более сложному сценарию, сочетающему использование $emit для передачи данных от дочернего компонента к родительскому. Допустим, у нас есть форма регистрации пользователя. Когда пользователь завершает регистрацию, необходимо уведомить об этом родительский компонент:

<template>
  <div class="registration-form">
    <input v-model="username" type="text" placeholder="Введите имя пользователя"/>
    <input v-model="email" type="email" placeholder="Введите email"/>
    <button @click="submitForm">Зарегистрироваться</button>
  </div>
</template>

<script>
export default {
  name: 'RegistrationForm',
  data() {
    return {
      username: '',
      email: ''
    };
  },
  methods: {
    submitForm() {
      const userData = {
        username: this.username,
        email: this.email
      };
      this.$emit('userRegistered', userData);
    }
  }
};
</script>

<style scoped>
.registration-form {
  margin: 20px;
}
</style>

<!-- Родительский компонент -->
<template>
  <div id="app">
    <RegistrationForm @userRegistered="handleUserRegistration"/>
    <p v-if="registeredUser">
      Пользователь зарегистрирован: {{ registeredUser.username }} ({{ registeredUser.email }})
    </p>
  </div>
</template>

<script>
import RegistrationForm from './components/RegistrationForm.vue';

export default {
  name: 'App',
  components: {
    RegistrationForm
  },
  data() {
    return {
      registeredUser: null
    };
  },
  methods: {
    handleUserRegistration(userData) {
      this.registeredUser = userData;
    }
  }
};
</script>

<style>
/* Ваши глобальные стили */
</style>

Здесь дочерний компонент (RegistrationForm) использует метод $emit, чтобы отправить данные регистрации пользователю (объект userData) родительскому компоненту через событие userRegistered.

Таким образом, комбинация использования методов $emit и рефлексивного доступа через директивы ref позволяет создавать гибкие схемы взаимодействия между компонентами. Эти инструменты подобны перу великого писателя — они дают возможность строить богатое интерактивное повествование в ваших приложениях Vue3.

Динамические компоненты в Vue.js

Введение в динамические компоненты

Как в увлекательной книге, где каждый персонаж может неожиданно раскрыть свою истинную сущность, так и в мире Vue3 динамические компоненты открывают безграничные возможности для создания гибких и адаптивных интерфейсов.

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

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

<template>
  <div id="app">
    <component :is="currentComponent"></component>
    <button @click="switchComponent('NewsComponent')">Новости</button>
    <button @click="switchComponent('ArticleComponent')">Статьи</button>
    <button @click="switchComponent('VideoComponent')">Видео</button>
  </div>
</template>

<script>
import NewsComponent from './components/NewsComponent.vue';
import ArticleComponent from './components/ArticleComponent.vue';
import VideoComponent from './components/VideoComponent.vue';

export default {
  name: 'App',
  components: {
    NewsComponent,
    ArticleComponent,
    VideoComponent
  },
  data() {
    return {
      currentComponent: 'NewsComponent'
    };
  },
methods:{
switchComponent(componentName){
this.currentComponent = componentName;}
}
};
</script>

<style> /* Ваши глобальные стили */ </style>

В этом примере мы используем встроенный компонент <component> с директивой is, которая позволяет указать имя текущего компонента для отображения. Кнопки вызывают метод switchComponent, который обновляет значение currentComponent, переключая тем самым видимый компонент на странице.

Теперь давайте углубимся в более сложный случай использования динамических компонентов — создание табов (вкладок), где каждая вкладка представляет собой отдельный компонент:

<template>
  <div id="tabs">
    <div class="tabs-header">
      <button v-for="tab in tabs" :key="tab" @click="selectTab(tab)">
        {{ tab }}
      </button>
    </div>
    <div class="tabs-content">
      <component :is="selectedTab"></component>
    </div>
  </div>
</template>

<script>
import TabHome from './components/TabHome.vue';
import TabProfile from './components/TabProfile.vue';
import TabSettings from './components/TabSettings.vue';

export default {
  name: 'Tabs',
  components: {
    TabHome,
    TabProfile,
    TabSettings
  },
  data() {
    return {
      tabs: ['Home', 'Profile', 'Settings'],
      selectedTab: 'TabHome'
    };
  },
  methods: {
    selectTab(tab) {
      this.selectedTab = `Tab${tab}`;
    }
  }
};
</script>

<style scoped>
.tabs-header button {
  margin-right: 10px;
  padding: 10px;
}
.tabs-content {
  margin-top: 20px;
}
</style>

Здесь у нас есть три вкладки (Home, Profile, Settings), каждая из которых соответствует своему компоненту (TabHome, TabProfile, TabSettings). При выборе вкладки метод selectTab обновляет значение переменной selectedTab, что приводит к отображению соответствующего компонента во вложенном элементе <component>.

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

Использование keep-alive

Как в театре, где за кулисами остаются сценические декорации, готовые вновь появиться на сцене по мере необходимости, так и в Vue3 существует механизм для сохранения состояния динамических компонентов при их переключении. Эта директива называется keep-alive, и она позволяет нам сохранять состояние компонент даже после того, как они были удалены из DOM.

Рассмотрим следующий пример, где мы создаем приложение с вкладками (табы), используя динамические компоненты. В этом примере вкладки будут сохранять своё состояние благодаря keep-alive.

<template>
  <div id="tabs">
    <div class="tabs-header">
      <button v-for="tab in tabs" :key="tab" @click="selectTab(tab)">
        {{ tab }}
      </button>
    </div>
    <div class="tabs-content">
      <!-- Используем keep-alive для сохранения состояния компонентов -->
      <keep-alive>
        <component :is="selectedTab"></component>
      </keep-alive>
    </div>
  </div>
</template>

<script>
import TabHome from './components/TabHome.vue';
import TabProfile from './components/TabProfile.vue';
import TabSettings from './components/TabSettings.vue';

export default {
  name: 'Tabs',
  components: {
    TabHome,
    TabProfile,
    TabSettings
  },
  data() {
    return {
      tabs: ['Home', 'Profile', 'Settings'],
      selectedTab: 'TabHome'
    };
  },
  methods: {
    selectTab(tab) {
      this.selectedTab = `Tab${tab}`;
    }
  }
};
</script>

<style scoped>
.tabs-header button {
  margin-right: 10px;
  padding: 10px;
}
.tabs-content {
  margin-top: 20px;
}
</style>

В этом примере мы оборачиваем наш динамический компонент в <keep-alive>. Это гарантирует, что состояние каждого компонента сохраняется между переключениями. Например, если пользователь введет данные в форме во вкладке "Profile", а затем переключится на вкладку "Settings", то при возвращении обратно на "Profile" введенные данные не исчезнут.

Теперь давайте рассмотрим еще один пример использования keep-alive в контексте более сложного приложения. Представьте себе приложение для управления задачами, где каждая задача открывается в отдельной вкладке и содержит форму редактирования:

<template>
  <div id="task-manager">
    <div class="task-tabs">
      <button v-for="task in tasks" :key="task.id" @click="selectTask(task.id)">
        {{ task.title }}
      </button>
    </div>
    <div class="task-details">
      <!-- Используем keep-alive для сохранения состояния формы редактирования задачи -->
      <keep-alive>
        <component :is="selectedTaskComponent.is" v-bind="selectedTaskComponent.props"></component>
      </keep-alive>
    </div>
  </div>
</template>

<script>
import TaskDetails from './components/TaskDetails.vue';

export default {
  name: 'TaskManager',
  components: {
    TaskDetails
  },
  data() {
    return {
      tasks: [
        { id: 1, title: 'Задача 1' },
        { id: 2, title: 'Задача 2' },
        { id: 3, title: 'Задача 3' }
      ],
      selectedTaskId: null
    };
  },
  computed: {
    selectedTaskComponent() {
      if (this.selectedTaskId) {
        return {
          is: 'TaskDetails',
          props: { taskId: this.selectedTaskId }
        };
      } else {
        return null;
      }
    }
  },
  methods: {
    selectTask(taskId) {
      this.selectedTaskId = taskId;
    }
  }
};
</script>

<style scoped>
.task-tabs button {
  margin-right: 10px;
  padding: 10px;
}
.task-details {
  margin-top: 20px;
}
</style>

В данном случае у нас есть список задач, каждая из которых может быть выбрана для просмотра и редактирования. Компонент TaskDetails отвечает за отображение деталей выбранной задачи. Благодаря использованию keep-alive, любые изменения или введенные данные сохраняются даже при переключении между различными задачами.

Таким образом, директива keep-alive является инструментом для улучшения пользовательского опыта в приложениях Vue3. Она позволяет сохранить состояние компонентов и избежать ненужных перезагрузок данных или повторного рендеринга компонентов при их переключении. Подобно мастерскому писателю, который бережно развивает характеры своих героев на протяжении всей истории, вы можете использовать эту директиву для создания плавного и интуитивно понятного интерфейса вашего приложения.

Реализация динамических форм

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

Начнем с простого примера динамической формы, которая изменяется в зависимости от выбранного типа пользователя. Представьте себе регистрацию на веб-сайте, где разные поля отображаются для физических лиц и компаний:

<template>
  <div id="app">
    <h2>Регистрация</h2>
    <form @submit.prevent="submitForm">
      <label>
        Тип пользователя:
        <select v-model="userType">
          <option value="individual">Физическое лицо</option>
          <option value="company">Компания</option>
        </select>
      </label>

      <!-- Общие поля -->
      <div class="common-fields">
        <label>
          Имя:
          <input type="text" v-model="formData.name" required />
        </label>
        <label>
          Email:
          <input type="email" v-model="formData.email" required />
        </label>
      </div>

      <!-- Динамические поля -->
      <component :is="dynamicComponent"></component>

      <!-- Кнопка отправки -->
      <button type="submit">Зарегистрироваться</button>
    </form>
  </div>
</template>

<script>
// Компонент для физических лиц
const IndividualFields = {
  template: `
    <div class="individual-fields">
      <label>
        Фамилия:
        <input type="text" v-model="$parent.formData.lastName" required />
      </label>
    </div>`
};

// Компонент для компаний
const CompanyFields = {
  template: `
    <div class="company-fields">
      <label>
        Название компании:
        <input type="text" v-model="$parent.formData.companyName" required />
      </label>
    </div>`
};

export default {
  name: 'App',
  components: {
    IndividualFields,
    CompanyFields
  },
  data() {
    return {
      userType: 'individual',
      formData: {
        name: '',
        email: '',
        lastName: '',
        companyName: ''
      }
    };
  },
  computed: {
    dynamicComponent() {
      return this.userType === 'individual' ? 'IndividualFields' : 'CompanyFields';
    }
  },
  methods: {
    submitForm() {
      console.log('Форма отправлена:', this.formData);
      
     // Здесь можно добавить логику обработки данных формы
      
     alert('Форма успешно отправлена!');
     
     // Сбрасываем данные формы после отправки
     this.formData = { name:'', email:'', lastName:'', companyName:'' };
     this.userType = 'individual';
   }
 }
};
</script>

<style scoped>.common-fields label,.individual-fields label,.company-fields label{ display:block; margin-bottom:10px; } button{ padding:10px; margin-top:20px; }</style> 

В этом примере мы создали два отдельных компонента IndividualFields и CompanyFields, которые используются внутри основного компонента App. В зависимости от выбранного типа пользователя (userType), соответствующий компонент загружается с помощью директивы is. Это позволяет форме динамически адаптироваться к выбору пользователя.

Теперь добавим валидацию данных формы перед их отправкой. Для этого используем библиотеку Vuelidate — инструмент для декларативной валидации данных:

npm install @vuelidate/core @vuelidate/validators --save

После установки библиотеки обновим наш код:

<template> 
  <div id="app"> 
    <h2>Регистрация</h2> 
    <form @submit.prevent="submitForm"> 
      <label>Тип пользователя: 
        <select v-model="userType"> 
          <option value="individual">Физическое лицо</option> 
          <option value="company">Компания</option> 
        </select>
      </label>

      <!-- Общие поля --> 
      <div class="common-fields">
        <label>Имя:
          <input type="text" v-model="formData.name" :class="{ invalid: $v.formData.name.$error }"/> 
          <span v-if="!$v.formData.name.required">Это поле обязательно.</span>
          <span v-if="!$v.formData.name.minLength">Имя должно содержать как минимум три символа.</span>
        </label>

        <label>Email:
          <input type="email" v-model="formData.email" :class="{ invalid: $v.formData.email.$error }"/>
          <span v-if="!$v.formData.email.required">Это поле обязательно.</span>
          <span v-if="!$v.formData.email.email">Введите корректный email.</span>
        </label>
      </div>

      <!-- Динамические поля --> 
      <component :is="dynamicComponent"/>

      <!-- Кнопка отправки --> 
      <button type="submit" :disabled="$v.$invalid">Зарегистрироваться</button>

      <div v-if="$v.$error" class="error-message">
        <p>Пожалуйста, исправьте ошибки перед отправкой формы.</p>
      </div>
    </form>
  </div>
</template>

<script>
import { required, minLength, email } from '@vuelidate/validators';
import useVuelidate from '@vuelidate/core';

const IndividualFields = {
  template: `
    <div class="individual-fields">
      <label>Фамилия:
        <input type="text" v-model="$parent.formData.lastName" :class="{ invalid: $parent.$v.formData.lastName.$error }"/>
        <span v-if="!$parent.$v.formData.lastName.required">Это поле обязательно.</span>
      </label>
    </div>
  `
};

const CompanyFields = {
  template: `
    <div class="company-fields">
      <label>Название компании:
        <input type="text" v-model="$parent.formData.companyName" :class="{ invalid: $parent.$v.formData.companyName.$error }"/>
        <span v-if="!$parent.$v.formData.companyName.required">Это поле обязательно.</span>
      </label>
    </div>
  `
};

export default {
  name: 'App',
  components: {
    IndividualFields,
    CompanyFields
  },
  data() {
    return {
      userType: 'individual',
      formData: {
        name: '',
        email: '',
        lastName: '',
        companyName: ''
      }
    };
  },
  computed: {
    dynamicComponent() {
      return this.userType === 'individual' ? 'IndividualFields' : 'CompanyFields';
    }
  },
  methods: {
    submitForm() {
      if (this.$v.$validate()) {
        console.log('Форма отправлена:', this.formData);
        alert('Форма успешно отправлена!');
        this.formData = { name: '', email: '', lastName: '', companyName: '' };
        this.userType = 'individual';
      } else {
        alert('Пожалуйста, исправьте ошибки перед отправкой формы');
      }
    }
  },
  validations() {
    return {
      userType: {},
      formData: {
        name: { required, minLength: minLength(3) },
        email: { required, email },
        lastName: { required },
        companyName: { required }
      }
    };
  }
};
</script>

Во-первых, необходимо импортировать необходимые валидаторы из пакета `@ vuelidate / validators` и установить глобальную директиву использования Vuelidate. Затем добавим правила валидации к нашим полям ввода, используя директиву `$vl`.

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

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

Слоты в компонентах

Основы использования слотов

В мире разработки пользовательских интерфейсов, слоты в Vue3 выступают в роли своеобразных "окон", через которые мы можем передавать и динамически изменять контент внутри компонентов. Эта функция позволяет создавать гибкие и переиспользуемые компоненты, способные адаптироваться к различным сценариям использования.

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

Начнем с базового примера использования слотов. Рассмотрим компонент ProductCard, который будет служить контейнером для отображения информации о продукте:

<template>
  <div class="product-card">
    <h2><slot name="title"></slot></h2>
    <p><slot name="description"></slot></p>
  </div>
</template>

<script>
export default {
  name: 'ProductCard'
};
</script>

<style scoped>
.product-card {
  border: 1px solid #ccc;
  padding: 20px;
}
</style>

В данном компоненте мы определили два именованных слота — title и description. Эти слоты будут заполняться контентом при использовании компонента:

<template>
  <div id="app">
    <ProductCard>
      <template v-slot:title>Смартфон XYZ</template>
      <template v-slot:description>Мощный и стильный смартфон с большим экраном.</template>
    </ProductCard>

    <ProductCard>
      <template v-slot:title>Ноутбук ABC</template>
      <template v-slot:description>Идеальный выбор для работы и развлечений.</template>
    </ProductCard>
  </div>
</template>

<script>
import ProductCard from './components/ProductCard.vue';

export default {
  name: 'App',
  components: {
    ProductCard
  }
};
</script>

<style scoped>
/* Ваши глобальные стили */
</style>

Как видно из примера, мы использовали директиву v-slot для заполнения именованных слотов в компоненте ProductCard. Это позволяет нам гибко управлять содержимым каждого экземпляра компонента.

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

<template>
  <div class="product-card">
    <h2><slot name="title">Без названия</slot></h2>
    <p><slot name="description">Описание отсутствует.</slot></p>
  </div>
</template>

<script>
export default {
  name: 'ProductCard'
};
</script>

<style scoped>.product-card { border:1px solid #ccc; padding :20px ;}</style >

Теперь, если пользователь не предоставит контент для одного из слотов, будет отображено дефолтное значение.

Иногда возникает необходимость передачи данных из родительского компонента внутрь слот-контента. Для этого используется так называемый "scoped slots". Рассмотрим пример создания списка задач с возможностью динамического изменения статуса каждой задачи:

<template>
  <div class="task-list">
    <ul>
      <li v-for="task in tasks" :key="task.id">
        <!-- Используем scoped slot -->
        <slot :task="task">{{ task.title }}</slot>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'TaskList',
  props: {
    tasks: Array
  }
};
</script>

<style scoped>
.task-list ul {
  list-style-type: none;
}
.task-list li {
  margin-bottom: 10px;
}
</style>

Родительский компонент может использовать этот слот следующим образом:

<template>
  <div id="app">
    <TaskList :tasks="tasks">
      <template v-slot="{ task }">
        {{ task.title }} - {{ task.status }}
        <button @click="changeStatus(task)">Изменить статус</button>
      </template>
    </TaskList>
  </div>
</template>

<script>
import TaskList from './components/TaskList.vue';

export default {
  name: 'App',
  components: {
    TaskList
  },
  data() {
    return {
      tasks: [
        { id: 1, title: 'Задача 1', status: 'В процессе' },
        { id: 2, title: 'Задача 2', status: 'Завершена' }
      ]
    };
  },
  methods: {
    changeStatus(task) {
      if (task.status === 'В процессе') {
        task.status = 'Завершена';
      } else {
        task.status = 'В процессе';
      }
    }
  }
};
</script>

<style scoped>
/* Ваши глобальные стили */
</style>

Используя директиву v-slot, мы можем получить доступ к данным задачи (task) прямо внутри слот-контента и динамически взаимодействовать с ними.

Scoped Slots

В мире Vue3, scoped slots предоставляют инструмент для передачи данных из компонента в слот и их последующего отображения. Эта концепция позволяет создавать ещё более гибкие и динамические компоненты, которые могут адаптироваться к различным сценариям использования.

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

Начнем с создания компонента TaskList, который будет принимать массив задач через props и использовать scoped slot для передачи каждой задачи наружу:

<template>
  <div class="task-list">
    <ul>
      <li v-for="task in tasks" :key="task.id">
        <!-- Используем scoped slot -->
        <slot :task="task">{{ task.title }}</slot>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'TaskList',
  props: {
    tasks: Array
  }
};
</script>

<style scoped>
.task-list ul {
  list-style-type: none;
}
.task-list li {
  margin-bottom: 10px;
}
</style> 

В данном примере мы создали компонент TaskList, который принимает список задач через props и передает каждую задачу в слот через директиву v-slot. Обратите внимание на использование выражения slot :task="task" — это позволяет нам передавать данные задачи (в нашем случае объект task) во внешний шаблон.

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

<template>
  <div id="app">
    <TaskList :tasks="tasks">
      <template v-slot="{ task }">
        {{ task.title }} - {{ task.status }}
        <button @click="changeStatus(task)">Изменить статус</button>
      </template>
    </TaskList>
  </div>
</template>

<script>
import TaskList from './components/TaskList.vue';

export default {
  name: 'App',
  components: {
    TaskList
  },
  data() {
    return {
      tasks: [
        { id: 1, title: 'Задача 1', status: 'В процессе' },
        { id: 2, title: 'Задача 2', status: 'Завершена' }
      ]
    };
  },
  methods: {
    changeStatus(task) {
      if (task.status === 'В процессе') {
        task.status = 'Завершена';
      } else {
        task.status = 'В процессе';
      }
    }
  }
};
</script>

<style scoped>
/* Ваши глобальные стили */
</style>

Родительский компонент использует директиву v-slot для получения данных задачи ({ task }) из слота. Затем он динамически взаимодействует с этими данными внутри своего шаблона — выводит название и статус задачи, а также предоставляет кнопку для изменения статуса.

Scoped slots позволяют не только получать данные из дочернего компонента, но и применять любую необходимую логику или форматирование прямо внутри родительского шаблона. Это делает ваши компоненты невероятно гибкими и переиспользуемыми.

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

<template>
  <div id="app">
    <TaskList :tasks="tasks">
      <template v-slot="{ task }">
        <strong>{{ task.title }}</strong> - {{ task.status }}
        <button @click="changeStatus(task)">Изменить статус</button>
        <p v-if="task.status === 'Завершена'">Эта задача завершена. Отличная работа!</p>
      </template>
    </TaskList>
  </div>
</template>

<script>
import TaskList from './components/TaskList.vue';

export default {
  name: 'App',
  components: {
    TaskList
  },
  data() {
    return {
      tasks: [
        { id: 1, title: 'Задача 1', status: 'В процессе' },
        { id: 2, title: 'Задача 2', status: 'Завершена' }
      ]
    };
  },
  methods: {
    changeStatus(task) {
      if (task.status === 'В процессе') {
        task.status = 'Завершена';
      } else {
        task.status = 'В процессе';
      }
    }
  }
};
</script>

<style scoped>
/* Ваши глобальные стили */
</style>

Паттерны использования слотов

Паттерн "Single Slot" (Один слот)

Один из самых простых паттернов — это использование одного слота для вставки контента в компонент. Этот подход особенно полезен для небольших компонентов, где необходима минимальная кастомизация.

<template>
  <div class="simple-container">
    <slot></slot>
  </div>
</template>

<script>
export default {
  name: 'SimpleContainer'
};
</script>

<style scoped>
.simple-container {
  padding: 20px;
  border: 1px solid #ccc;
}
</style>

В данном примере компонент SimpleContainer имеет единственный слот, куда можно вставить любой контент:

<template>
  <div id="app">
    <SimpleContainer>
      <p>Это простой пример использования одного слота.</p>
    </SimpleContainer>
  </div>
</template>

<script>
import SimpleContainer from './components/SimpleContainer.vue';

export default {
  name: 'App',
  components: {
    SimpleContainer
  }
};
</script>

<style scoped>
/* Ваши глобальные стили */
</style>

Паттерн "Named Slots" (Именованные слоты)

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

<template>
  <div class="card">
    <header><slot name="header"></slot></header>
    <main><slot name="content"></slot></main>
    <footer><slot name="footer"></slot></footer>
  </div>
</template>

<script>
export default {
  name: 'Card'
};
</script>

<style scoped>
.card {
  border: 1px solid #ccc;
  padding: 20px;
}
</style>

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

<template>
  <div id="app">
    <Card>
      <template v-slot:header>
        <h2>Заголовок карточки</h2>
      </template>
      <template v-slot:content>
        <p>Это основной контент карточки. Он может содержать текст, изображения и многое другое.</p>
      </template>
      <template v-slot:footer>
        <button @click="handleClick">Нажми меня</button>
      </template>
    </Card>
  </div>
</template>

<script>
import Card from './components/Card.vue';

export default {
  name: 'App',
  components: {
    Card
  },
  methods: {
    handleClick() {
      alert('Кнопка нажата!');
    }
  }
};
</script>

<style scoped>
/* Ваши глобальные стили */
</style>

Паттерн "Scoped Slots with Named Slots" (Scoped слоты с именованными слотами)

Иногда возникает необходимость не только вставить контент в определенные области компонента, но и передать данные из компонента наружу через scoped slots вместе с именованными слотами. Такой подход позволяет создавать очень гибкие и динамические интерфейсы.

<template>
  <div class="user-profile">
    <header><slot name="header" :user="user"></slot></header>
    <main><slot name="content" :user="user"></slot></main>
    <footer><slot name="footer" :user="user"></slot></footer>
  </div>
</template>

<script>
export default {
  name: 'UserProfile',
  data() {
    return {
      user: {
        name: 'Иван Иванов',
        age: 30,
        email: 'ivan@example.com'
      }
    };
  }
};
</script>

<style scoped>
.user-profile {
  border: 1px solid #ccc;
  padding: 20px;
}
header, main, footer {
  margin-bottom: 10px;
}
</style>

Родительский компонент может использовать эти данные для отображения различного контента в зависимости от свойств пользователя:

<template>
  <div id="app">
    <UserProfile>
      <template v-slot:header="{ user }">
        <h2>{{ user.name }}</h2>
      </template>
      <template v-slot:content="{ user }">
        <p>{{ user.email }}</p>
      </template>
      <template v-slot:footer="{ user }">
        <button @click="sendMessage(user)">Отправить сообщение</button>
      </template>
    </UserProfile>
  </div>
</template>

<script>
import UserProfile from './components/UserProfile.vue';

export default {
  name: 'App',
  components: {
    UserProfile
  },
  methods: {
    sendMessage(user) {
      alert('Сообщение отправлено пользователю ' + user.name);
    }
  }
};
</script>

<style scoped>
/* Ваши глобальные стили */
</style>

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

Паттерн "Dynamic Content Injection" (Динамическая инъекция контента)

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

<template>
  <div id="tabs-container">
    <ul class="tabs-header">
      <li v-for="(tab, index) in tabs" @click="selectTab(index)">
        {{ tab.title }}
      </li>
    </ul>
    <!-- Динамическое содержимое табов -->
    <div class="tabs-content">
      <slot :currentTabContent="currentTabContent">{{ currentTabContent }}</slot>
    </div>
  </div>
</template>

<script>
export default {
  name: 'TabsContainer',
  data() {
    return {
      tabs: [
        { title: 'Таб 1', content: 'Контент таба 1' },
        { title: 'Таб 2', content: 'Контент таба 2' }
      ],
      selectedIndex: 0
    };
  },
  computed: {
    currentTabContent() {
      return this.tabs[this.selectedIndex].content;
    }
  },
  methods: {
    selectTab(index) {
      this.selectedIndex = index;
    }
  }
};
</script>

<style scoped>
.tabs-header li {
  cursor: pointer;
  display: inline-block;
  margin-right: 10px;
  padding: 10px;
}
.tabs-content {
  margin-top: 20px;
}
</style>

Теперь родительский компонент может использовать этот контейнер для отображения различных вкладок:

<template>
  <div id="app">
    <TabsContainer>
      <template v-slot="{ currentTabContent }">
        {{ currentTabContent }}
      </template>
    </TabsContainer>
  </div>
</template>

<script>
import TabsContainer from './components/TabsContainer.vue';

export default {
  name: 'App',
  components: {
    TabsContainer
  }
};
</script>

<style scoped>
/* Ваши глобальные стили */
</style>

Такой подход обеспечивает максимальную гибкость при работе с динамическим содержимым.


Читайте также:

ChatGPT
Eva
💫 Eva assistant