Что вызывает сбой анимации в этом приложении Vue 3?

#javascript #vue.js #vuejs3

Вопрос:

Я работаю над пользовательским приложением CRUD с Vue 3.

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

Для этой цели я сделал следующее:

Шаблон:

 <tbody name="users-table" is="transition-group">
  <tr v-for="user in reversedUsers" :key="user.id" class="user-row">
    <td>{{user.first_name}}</td>
    <td>{{user.last_name}}</td>
    <td>{{user.email}}</td>
  </tr>
</tbody>
 

CSS

 .user-row {
  transition: all 1s ease;
  opacity: 1;
  height: auto;
}

.user-row-active {
  opacity: 0;
  height: 0;
}
 

CSS может быть «невпечатляющим», но реальная проблема в том, что классы, которые я ожидал, что Vue добавит в анимируемый элемент, даже не добавлены:

 const usersApp = {
  data() {
    return {
      users: [{
          id: 1,
          first_name: "John",
          last_name: "Doe",
          email: "john.doe@gmail.com"
        },
        {
          id: 2,
          first_name: "Jane",
          last_name: "Doe",
          email: "jane.doe@gmail.com"
        }
      ],
      formData: {
        first_name: "",
        last_name: "",
        email: ""
      },
      formErrors: [],
      userAdded: false
    };
  },
  methods: {
    isNotEmpty() {
      return (
        this.formData.first_name amp;amp;
        this.formData.last_name amp;amp;
        this.formData.email
      );
    },
    isEmail(email) {
      return String(email)
        .toLowerCase()
        .match(
          /^(([^<>()[]\.,;:s@"] (.[^<>()[]\.,;:s@"] )*)|(". "))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9] .) [a-zA-Z]{2,}))$/
        );
    },
    formValidation() {
      this.formErrors = [];

      if (!this.isNotEmpty()) {
        this.formErrors.push("There are empty filelds");
      }

      if (this.isNotEmpty() amp;amp; !this.isEmail(this.formData.email)) {
        this.formErrors.push("The email is invalid");
      }
    },
    resetAddFormData() {
      this.formData.first_name = "";
      this.formData.last_name = "";
      this.formData.email = "";
      this.userAdded = false;
      this.formErrors = [];
    },
    addUser() {
      // Validate form data
      this.formValidation();

      // Make New User
      let newUser = {
        first_name: this.formData.first_name,
        last_name: this.formData.last_name,
        email: this.formData.email
      };

      // Add new user
      if (!this.formErrors.length) {
        this.users.push(newUser);
        this.userAdded = true;
        window.setTimeout(this.resetAddFormData, 500);
      }
    }
  },
  watch: {
    users() {
      this.users();
    }
  },
  computed: {
    reversedUsers() {
      return this.users.slice().reverse();
    }
  }
};

Vue.createApp(usersApp).mount("#app"); 
 .logo {
  width: 30px;
}

.nav-item {
  width: 100%;
}

@media (min-width: 768px) {
  .nav-item {
    width: auto;
  }
}

.user-row {
  transition: all 1s ease;
  opacity: 1;
  height: auto;
}

.user-row-active {
  opacity: 0;
  height: 0;
}

.alert {
  padding: 0.6rem 0.75rem;
  text-align: center;
  font-weight: 600;
} 
 <link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script src="https://unpkg.com/vue@next"></script>


<div id="app">
  <nav class="navbar navbar-expand-sm bg-dark navbar-dark">
    <!-- Brand -->
    <a class="navbar-brand p-0" href="#">
      <img src="https://www.pngrepo.com/png/303293/180/bootstrap-4-logo.png" alt="" class="logo">
    </a>

    <!-- Links -->
    <div class="navbar-nav w-100">
      <ul class="navbar-nav ml-auto" id="navbarSupportedContent">
        <li class="nav-item">
          <button type="button" class="btn btn-sm btn-success" data-toggle="modal" data-target="#exampleModalCenter">
            Add user
          </button>
        </li>
      </ul>
    </div>
  </nav>

  <div class="container">
    <div class="card my-2">
      <h5 class="card-header px-2">Users</h5>
      <div class="card-body p-0">
        <table class="table table-striped m-0">
          <thead>
            <tr>
              <th>Firstname</th>
              <th>Lastname</th>
              <th>Email</th>
            </tr>
          </thead>
          <tbody name="users-table" is="transition-group">
            <tr v-for="user in reversedUsers" :key="user.id" class="user-row">
              <td>{{user.first_name}}</td>
              <td>{{user.last_name}}</td>
              <td>{{user.email}}</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>

  <div class="modal fade" id="exampleModalCenter" tabindex="-1" role="dialog" aria-labelledby="exampleModalCenterTitle" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <h3 class="modal-title h6" id="exampleModalLongTitle">Add New User</h3>
          <button type="button" class="close" data-dismiss="modal" aria-label="Close" @click="resetAddFormData">
            <span aria-hidden="true">amp;times;</span>
          </button>
        </div>
        <div class="modal-body">
          <div v-if="this.formErrors.length">
            <div v-for="error in formErrors" class="alert alert-danger">
              {{error}}
            </div>
          </div>
          <div v-if="userAdded" class="alert alert-success">User added successfully!</div>
          <form @submit.prevent="addUser" novalidate>
            <div class="form-group mb-2">
              <input type="text" class="form-control input-sm" placeholder="First name" v-model="formData.first_name">
            </div>
            <div class="form-group mb-2">
              <input type="text" class="form-control input-sm" placeholder="Last name" v-model="formData.last_name">
            </div>
            <div class="form-group mb-2">
              <input type="email" class="form-control input-sm" placeholder="Email address" v-model="formData.email">
            </div>
            <div class=" mt-2">
              <button type="submit" class="btn btn-sm btn-block btn-success">Submit</button>
            </div>
          </form>
        </div>
      </div>
    </div>
  </div>
</div> 

Что я делаю не так?

Ответ №1:

Я использую приложение Todo в качестве примера в способе api композиции с vue-cli

// столовая: src/компоненты/Todos.vue

 <template>
  <div class="todos">
    <input
      type="text"
      v-model="newTodo"
      @keypress.enter="addTodo"
      placeholder="Add a new todo..."
    />
    <transition name="switch" mode="out-in">
      <div v-if="todos.length">
        <transition-group tag="ul" name="list" appear>
          <li v-for="todo in todos" :key="todo.id" @click="deleteTodo(todo.id)">
            {{ todo.text }}
          </li>
        </transition-group>
      </div>
      <div v-else>Woohoo, nothing left todo!</div>
    </transition>
  </div>
</template>

<script>
import { ref } from "vue";

export default {
  setup(props, { emit }) {
    const todos = ref([
      { text: "make the bed", id: 1 },
      { text: "play video games", id: 2 },
    ]);
    const newTodo = ref("");

    const addTodo = () => {
      if (newTodo.value) {
        const id = Math.random();
        todos.value = [{ text: newTodo.value, id }, ...todos.value];
        newTodo.value = "";
      } else {
        emit("badValue");
      }
    };

    const deleteTodo = (id) => {
      todos.value = todos.value.filter((todo) => todo.id != id);
    };

    return { todos, addTodo, deleteTodo, newTodo };
  },
};
</script>

<style>
.todos {
  max-width: 400px;
  margin: 20px auto;
  position: relative;
}
input {
  width: 100%;
  padding: 12px;
  border: 1px solid #eee;
  border-radius: 10px;
  box-sizing: border-box;
  margin-bottom: 20px;
}
.todos ul {
  position: relative;
  padding: 0;
}
.todos li {
  list-style-type: none;
  display: block;
  margin-bottom: 10px;
  padding: 10px;
  background: white;
  box-shadow: 1px 3px 5px rgba(0, 0, 0, 0.1);
  border-radius: 10px;
  width: 100%;
  box-sizing: border-box;
}
.todos li:hover {
  cursor: pointer;
}

/* list transitions */
.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: scale(0.6);
}
.list-enter-active {
  transition: all 0.4s ease;
}
.list-leave-active {
  transition: all 0.4s ease;
  position: absolute;
}
.list-leave-to {
  opacity: 0;
  transform: scale(0.6);
}
.list-move {
  transition: all 0.3s ease;
}

/* switch transitions */
.switch-enter-from,
.switch-leave-to {
  opacity: 0;
  transform: translateY(20px);
}
.switch-enter-active
.switch-leave-active {
  transition: all 0.5s ease;
}
</style>

// directory: src/views/Home.views

<template>
  <div class="home">
    <Todos />
  </div>
</template>

<script>
import Todos from "../components/Todos";

export default {
  components: { Todos }
};
</script>

<style scoped>
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
  transition: opacity 2s ease;
}
</style>
 

Ответ №2:

У <transition-group> него есть tag реквизит, который определяет элемент для визуализации. Используйте эту опору tbody в своем столе:

 <!-- <tbody name="users-table" is="transition-group"> --> ❌

<transition-group name="users-table" tag="tbody"> ✅
 

И базовое имя CSS-класса для анимации должно соответствовать name реквизиту <transition-group> : users-table (не классу анимируемого элемента). Кроме того, имена классов перехода должны использоваться в именах классов:

 .users-table-enter-active,
.users-table-leave-active {
  transition: all 1s ease;
}
.users-table-enter-from,
.users-table-leave-to {
  opacity: 0;
  transform: translateY(30px);
}
 

ДЕМОНСТРАЦИЯ