Vue 反应性错误
Vue reactivity bug
先声明前端不是我的强项..
我有一个下拉组件,我想要的只是当我按下重置按钮时,它会恢复到占位符文本,而不是保留所选选项。
我不知道为什么这不起作用。我已经阅读了两天的文档并尝试了很多方法都无济于事。对我来说,它显然是反应性的,因为它在其他部分也是反应性的,但是当我将它设置为 null 时,没有反应性触发。
Dropdown.vue(主要来自 here,稍作修改)
<template>
<div class="btn-group">
<li @click="toggleMenu()" class="dropdown-toggle dropdown-toggle-placeholder" v-if="isPlaceholder()">
{{placeholderText}}
<span class="caret"></span>
</li>
<li @click="toggleMenu()" class="dropdown-toggle" v-else>
{{ selectedOption }}
<span class="caret"></span>
</li>
<ul class="dropdown-menu" v-if="showMenu">
<li v-for="(option, idx) in options" :key="idx">
<a href="javascript:void(0)" @click="updateOption(option)" draggable="false">
{{ option }}
</a>
</li>
</ul>
</div>
</template>
<script>
export default {
props: {
options: {
type: Array
},
selected: {
type: String
},
placeholder: {
type: String
},
closeOnOutsideClick: {
type: Boolean,
default: true,
},
},
data() {
return {
selectedOption: '',
showMenu: false,
placeholderText: 'Please select an item',
}
},
mounted() {
this.selectedOption = this.selected;
if (this.placeholder)
{
this.placeholderText = this.placeholder;
}
if (this.closeOnOutsideClick) {
document.addEventListener('click', this.clickHandler);
}
},
beforeUnmount() {
document.removeEventListener('click', this.clickHandler);
},
methods: {
updateOption(option) {
this.selectedOption = option;
this.showMenu = false;
this.$emit('update-option', this.selectedOption);
},
clearOption() {
this.selectedOption = null;
this.showMenu = false;
console.log("reset:", this.selectedOption);
},
toggleMenu() {
this.showMenu = !this.showMenu;
},
clickHandler(event) {
const { target } = event;
const { $el } = this;
if (!$el.contains(target)) {
this.showMenu = false;
}
},
isPlaceholder() {
return (this.selectedOption === undefined || this.selectedOption === null || this.selectedOption === '');
}
},
}
</script>
<style>
.btn-group {
min-width: 160px;
height: 40px;
position: relative;
margin: 10px 1px;
display: inline-block;
vertical-align: middle;
}
.btn-group a:hover {
text-decoration: none;
}
.dropdown-toggle {
color: #636b6f;
min-width: 160px;
padding: 10px 20px 10px 10px;
text-transform: none;
font-weight: 300;
margin-bottom: 7px;
border: 0;
background-image: linear-gradient(#009688, #009688), linear-gradient(#D2D2D2, #D2D2D2);
background-size: 0 2px, 100% 1px;
background-repeat: no-repeat;
background-position: center bottom, center calc(100% - 1px);
background-color: transparent;
transition: background 0s ease-out;
float: none;
box-shadow: none;
border-radius: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
user-select: none;
}
.dropdown-toggle:hover {
background: #e1e1e1;
cursor: pointer;
}
.dropdown-menu {
display: inherit !important;
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
list-style: none;
font-size: 14px;
text-align: left;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
background-clip: padding-box;
}
.dropdown-menu > li > a {
padding: 10px 30px;
display: block;
clear: both;
font-weight: normal;
line-height: 1.6;
color: #333333;
white-space: nowrap;
text-decoration: none;
user-select: none;
}
.dropdown-menu > li > a:hover {
background: #efefef;
color: #409FCB;
}
.dropdown-menu > li {
overflow: hidden;
width: 100%;
position: relative;
margin: 0;
}
.caret {
width: 0;
position: absolute;
top: 19px;
height: 0;
margin-left: -24px;
vertical-align: middle;
border-top: 4px dashed;
border-top: 4px solid ;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
right: 10px;
}
li {
list-style: none;
}
li.dropdown-toggle::after {
display: none;
}
</style>
App.vue
<template>
<form @reset.prevent="onReset">
<dropdown v-if="foo.bar" class="my-dropdown"
:options="myOptions"
:selected="foo.bar.name"
@update-option="onSelected"
:placeholder="'Select a MyOption'">
</dropdown>
<button type="reset">Reset</button>
</form>
</template>
<script>
import dropdown from './components/Dropdown.vue'
export default {
name: 'App',
components: {
dropdown,
},
data() {
return {
foo: {},
myOptions: [],
};
},
methods: {
onReset() {
console.log("Resetting...");
this.foo = {};
this.foo['bar'] = {};
this.foo['bar']['name'] = '';
dropdown.methods.clearOption();
},
onSelected(selected) {
this.foo.bar.name = selected;
console.log(selected);
}
},
created() {
this.onReset();
},
mounted() {
this.myOptions = ['A', 'B'];
}
}
</script>
<style scoped>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.my-dropdown {
border-radius: 5px;
::v-deep(.dropdown-toggle)
{
color: tomato;
font-size: 25px;
font-weight: 800;
}
::v-deep(.dropdown-toggle-placeholder) {
color: #c4c4c4;
}
}
</style>
当我按下重置按钮并调用 dropdown.methods.clearOption()
时,下拉菜单将 this.selectedOption
设置为 null
(我也尝试过空字符串等)。我所有的 console.logs 看起来都很好,看起来好像有效,但 v-if="isPlaceholder()"
和 v-else
就是不起作用。所选选项保留在保管箱中,而不是恢复为占位符文本。
我做错了什么????
根据您自己的代码假设 Vue 的反应性中存在错误,甚至没有对其进行测试...您真是太大胆了。我没有深入研究,因为我发现这真的很讨厌错误在你自己的代码中。您直接在 App 的实例中调用 dropdown.methods.clearOption();
函数。对于我来说,Vue 可以很好地解释它,但 JS 并不像您认为的那样工作。首先,使用类似的方法,您一路上丢失了 this context。
要在 Vue 中解决这个问题,您需要获取对您尝试调用的组件的引用。要在 Vue 中创建引用,需要在 App 组件的模板中添加一个 ref
属性:
<dropdown
ref="myDropDown"
v-if="foo.bar"
class="my-dropdown"
:options="myOptions"
:selected="foo.bar.name"
@update-option="onSelected"
:placeholder="'Select a MyOption'">
</dropdown>
然后可以通过myDropDown
引用到达组件实例,例如:
onReset() {
// the `?.` is an optional chaining operator
// can be easily replaced with an if statement
this.$refs.myDropDown?.clearOption();
}
此外,正如我注意到的,您在模板中使用了 v-if
语句,此时它可能会以缺少 $refs.myDropDown
引用而告终。要解决此问题,您可以通过 Vue.nextTick.
延迟调用该方法
// defers clearing the dropdown options
Vue.nextTick(() => this.$refs.myDropDown?.clearOption())
无论如何,编写这样的组件 - 与 Vue 中必须提供的反应性相反。您可以随时观察 props 的变化并使组件相应地运行,而无需获取任何引用。
先声明前端不是我的强项..
我有一个下拉组件,我想要的只是当我按下重置按钮时,它会恢复到占位符文本,而不是保留所选选项。 我不知道为什么这不起作用。我已经阅读了两天的文档并尝试了很多方法都无济于事。对我来说,它显然是反应性的,因为它在其他部分也是反应性的,但是当我将它设置为 null 时,没有反应性触发。
Dropdown.vue(主要来自 here,稍作修改)
<template>
<div class="btn-group">
<li @click="toggleMenu()" class="dropdown-toggle dropdown-toggle-placeholder" v-if="isPlaceholder()">
{{placeholderText}}
<span class="caret"></span>
</li>
<li @click="toggleMenu()" class="dropdown-toggle" v-else>
{{ selectedOption }}
<span class="caret"></span>
</li>
<ul class="dropdown-menu" v-if="showMenu">
<li v-for="(option, idx) in options" :key="idx">
<a href="javascript:void(0)" @click="updateOption(option)" draggable="false">
{{ option }}
</a>
</li>
</ul>
</div>
</template>
<script>
export default {
props: {
options: {
type: Array
},
selected: {
type: String
},
placeholder: {
type: String
},
closeOnOutsideClick: {
type: Boolean,
default: true,
},
},
data() {
return {
selectedOption: '',
showMenu: false,
placeholderText: 'Please select an item',
}
},
mounted() {
this.selectedOption = this.selected;
if (this.placeholder)
{
this.placeholderText = this.placeholder;
}
if (this.closeOnOutsideClick) {
document.addEventListener('click', this.clickHandler);
}
},
beforeUnmount() {
document.removeEventListener('click', this.clickHandler);
},
methods: {
updateOption(option) {
this.selectedOption = option;
this.showMenu = false;
this.$emit('update-option', this.selectedOption);
},
clearOption() {
this.selectedOption = null;
this.showMenu = false;
console.log("reset:", this.selectedOption);
},
toggleMenu() {
this.showMenu = !this.showMenu;
},
clickHandler(event) {
const { target } = event;
const { $el } = this;
if (!$el.contains(target)) {
this.showMenu = false;
}
},
isPlaceholder() {
return (this.selectedOption === undefined || this.selectedOption === null || this.selectedOption === '');
}
},
}
</script>
<style>
.btn-group {
min-width: 160px;
height: 40px;
position: relative;
margin: 10px 1px;
display: inline-block;
vertical-align: middle;
}
.btn-group a:hover {
text-decoration: none;
}
.dropdown-toggle {
color: #636b6f;
min-width: 160px;
padding: 10px 20px 10px 10px;
text-transform: none;
font-weight: 300;
margin-bottom: 7px;
border: 0;
background-image: linear-gradient(#009688, #009688), linear-gradient(#D2D2D2, #D2D2D2);
background-size: 0 2px, 100% 1px;
background-repeat: no-repeat;
background-position: center bottom, center calc(100% - 1px);
background-color: transparent;
transition: background 0s ease-out;
float: none;
box-shadow: none;
border-radius: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
user-select: none;
}
.dropdown-toggle:hover {
background: #e1e1e1;
cursor: pointer;
}
.dropdown-menu {
display: inherit !important;
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
list-style: none;
font-size: 14px;
text-align: left;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
background-clip: padding-box;
}
.dropdown-menu > li > a {
padding: 10px 30px;
display: block;
clear: both;
font-weight: normal;
line-height: 1.6;
color: #333333;
white-space: nowrap;
text-decoration: none;
user-select: none;
}
.dropdown-menu > li > a:hover {
background: #efefef;
color: #409FCB;
}
.dropdown-menu > li {
overflow: hidden;
width: 100%;
position: relative;
margin: 0;
}
.caret {
width: 0;
position: absolute;
top: 19px;
height: 0;
margin-left: -24px;
vertical-align: middle;
border-top: 4px dashed;
border-top: 4px solid ;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
right: 10px;
}
li {
list-style: none;
}
li.dropdown-toggle::after {
display: none;
}
</style>
App.vue
<template>
<form @reset.prevent="onReset">
<dropdown v-if="foo.bar" class="my-dropdown"
:options="myOptions"
:selected="foo.bar.name"
@update-option="onSelected"
:placeholder="'Select a MyOption'">
</dropdown>
<button type="reset">Reset</button>
</form>
</template>
<script>
import dropdown from './components/Dropdown.vue'
export default {
name: 'App',
components: {
dropdown,
},
data() {
return {
foo: {},
myOptions: [],
};
},
methods: {
onReset() {
console.log("Resetting...");
this.foo = {};
this.foo['bar'] = {};
this.foo['bar']['name'] = '';
dropdown.methods.clearOption();
},
onSelected(selected) {
this.foo.bar.name = selected;
console.log(selected);
}
},
created() {
this.onReset();
},
mounted() {
this.myOptions = ['A', 'B'];
}
}
</script>
<style scoped>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.my-dropdown {
border-radius: 5px;
::v-deep(.dropdown-toggle)
{
color: tomato;
font-size: 25px;
font-weight: 800;
}
::v-deep(.dropdown-toggle-placeholder) {
color: #c4c4c4;
}
}
</style>
当我按下重置按钮并调用 dropdown.methods.clearOption()
时,下拉菜单将 this.selectedOption
设置为 null
(我也尝试过空字符串等)。我所有的 console.logs 看起来都很好,看起来好像有效,但 v-if="isPlaceholder()"
和 v-else
就是不起作用。所选选项保留在保管箱中,而不是恢复为占位符文本。
我做错了什么????
根据您自己的代码假设 Vue 的反应性中存在错误,甚至没有对其进行测试...您真是太大胆了。我没有深入研究,因为我发现这真的很讨厌错误在你自己的代码中。您直接在 App 的实例中调用 dropdown.methods.clearOption();
函数。对于我来说,Vue 可以很好地解释它,但 JS 并不像您认为的那样工作。首先,使用类似的方法,您一路上丢失了 this context。
要在 Vue 中解决这个问题,您需要获取对您尝试调用的组件的引用。要在 Vue 中创建引用,需要在 App 组件的模板中添加一个 ref
属性:
<dropdown
ref="myDropDown"
v-if="foo.bar"
class="my-dropdown"
:options="myOptions"
:selected="foo.bar.name"
@update-option="onSelected"
:placeholder="'Select a MyOption'">
</dropdown>
然后可以通过myDropDown
引用到达组件实例,例如:
onReset() {
// the `?.` is an optional chaining operator
// can be easily replaced with an if statement
this.$refs.myDropDown?.clearOption();
}
此外,正如我注意到的,您在模板中使用了 v-if
语句,此时它可能会以缺少 $refs.myDropDown
引用而告终。要解决此问题,您可以通过 Vue.nextTick.
// defers clearing the dropdown options
Vue.nextTick(() => this.$refs.myDropDown?.clearOption())
无论如何,编写这样的组件 - 与 Vue 中必须提供的反应性相反。您可以随时观察 props 的变化并使组件相应地运行,而无需获取任何引用。