【问题标题】:Overriding input default behavior - Controlled <input /> element value and onChange event in VueJS覆盖输入默认行为 - VueJS 中受控的 <input /> 元素值和 onChange 事件
【发布时间】:2020-11-17 14:37:33
【问题描述】:

来自 React,我习惯于“控制” html 元素值和事件的概念。

我希望对 VueJS 做同样的事情。

我有,一个输入:

<input :value="foo" @input="changeFoo" pattern="[a-zA-Z0-9]+">

来自我的 VueX 商店的计算值:

...mapState({
 foo: (state) => state.foo,
}),

一种向我的 VueX 状态提交突变的方法

changeFoo(e) { this.$store.commit('CHANGE_FOO', e.target)}

如果正则表达式匹配,则更新状态的突变

CHANGE_FOO(state, target) {
  if (target.checkValidity()) {
    state.foo = target.value;
  }
 },

什么有效:

  • 我的 VueX 状态更新
  • 我的输入更新
  • 如果checkValidity返回false,则商店没有更新

什么不起作用:

  • 即使商店没有更新,&lt;input /&gt; 的值也会发生变化...我想要的是将单一事实源绑定到我的 &lt;input /&gt; 组件

【问题讨论】:

  • “即使store没有更新,&lt;input /&gt;的值也会发生变化”——它是如何变化的?
  • 您可能需要在不同的时候阻止默认行为,例如在有效性检查失败时。
  • 或者,尝试阻止 @keypress 的默认值,这可能是阻止某些字符被输入的最简单方法。
  • 实际上,@keypress 为我工作,我只是尝试过 :-) 如果您需要一些指针,请告诉我。
  • 我添加了我的变体。只是对您的答案的补充。他们都是伟大的! :-)

标签: html vue.js input vuejs3


【解决方案1】:

不要使用:value。将 v-model 与计算值一起使用:

<input v-model="foo" pattern="[a-zA-Z0-9]+" ref="fooInput">

computed: {
  foo: {
    get() {
      return this.$store.state.foo
    },
    set(value) {
      if (this.$refs.fooInput.checkValidity()) {
         this.$store.commit('CHANGE_FOO', value);
      } 
    }
  }
}

在 Vue 中,您不会“控制”。您引用引用 DOM 元素。

这是一种防止在用户输入无效值时更改输入值的方法。这被认为是不好的做法。

new Vue({
  el: '#app',
  data: () => ({
    storeFoo: null
  }),
  computed: {
    foo: {
      get() {
        return this.storeFoo
      }, 
      set(value) {
        if (this.$refs.fooInput.checkValidity()) {
          this.storeFoo = value;
        }
      }
    }
  },
  methods: {
    onInput(e) {
      if (!e.target.checkValidity()) {
        e.preventDefault();
        e.target.value = this.storeFoo || null;
      }
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <input v-model="foo" pattern="[a-zA-Z0-9]+" ref="fooInput" @input="onInput">
</div>

为简洁起见,使用 storeFoo 而不是适当的 store 道具。


我还应该解释为什么阻止输入更改是不好的做法:

就用户体验而言,您希望以一种尽可能友好(不突兀)的方式让用户知道当前值何时无效以及为什么无效。

如果您忽略或阻止他们的行为,则表明您对他们缺乏信任和尊重。

【讨论】:

  • 感谢@tao,通过这种方法学到了一些关于 $refs 的知识。但是问题仍然存在,例如您甚至可以 get() { return '' } 和 set(value) { // do nothing }。 Input 仍然会自行更新其值,而不是覆盖 html 组件的默认行为。至少在我的用例中
  • 您不想阻止输入默认行为。这让用户感到沮丧,并且基本上会让您(或您的客户)失去资金。您应该防止提交无效值,并让用户知道他们的值何时无效。这被认为是最佳实践。显然,您可能会比较激进,将输入值覆盖回之前的值,但我强烈建议您不要这样做。或者你可以防止默认。我会更新的。
  • 你做错了什么。如您所见,它按预期工作。 $refs 中没有你的意思是什么? $refs.fooInput 返回实际的 DOM 元素。您是说&lt;input&gt; 元素没有适合您的.checkValidity() 吗?这是极不可能的。
  • 我所说的是很常见的不良做法。但我绝对没有理由让你相信这一点。此外,我个人对验证的选择是使其尽可能接近&lt;input&gt;。除非我有多个输入源,否则我永远不会把它放在商店里。商店需要尽可能干净,只关心数据和业务逻辑。使用有效数据,我可能会添加(已经验证)。我发现这个原则非常有效。我发现自己使用这个原则可以更快地编写应用程序。但对你来说可能不一样。我不知道。
【解决方案2】:

通常我们会使用v-model 来双向绑定输入元素,但由于您的情况与input masking 更接近,因此一个快速的解决方法可能是对keypress 进行输入有效性检查事件而不是 input 或 change。这是一个快速演示(顺便说一句,我省略了 Vuex 变异实现):

Vue.createApp({
  setup() {
    const foo = Vue.ref('');
    const invalid = Vue.ref(false);
    
    const changeFoo = e => {
      if (!e.target.checkValidity()) {
        e.preventDefault();
        invalid.value = true;
      }
      else {
        invalid.value = false;
      }
    }

    return {
      foo,
      invalid,
      changeFoo
    }
  }
}).mount('#app');
.error {
  color: crimson;
}
<script src="https://unpkg.com/vue@next"></script>

<div id="app">
  <input 
    v-model="foo" 
    pattern="[a-zA-Z0-9]+"
    @keypress="changeFoo" />

  <p v-if="invalid" class="error">Pattern mismatch</p>
</div>

您可能会注意到在输入下一个按键之前不会出现错误消息。这是预期行为,与input 事件相当。

我们可以通过检查击键而不是整个输入值来解决这个问题。

Vue.createApp({
  setup() {
    const foo = Vue.ref('');
    const invalid = Vue.ref(false);
    const pattern = /[a-zA-Z0-9]+/;
    
    function changeFoo(e) {
      if (!checkValidity(e)) {
        e.preventDefault();
        invalid.value = true;
      }
      else {
        invalid.value = false;
      }
    }
    
    function checkValidity({ keyCode }) {
      return pattern.test(String.fromCharCode(keyCode));
    }

    return {
      pattern,
      foo,
      invalid,
      changeFoo
    }
  }
}).mount('#app');
.error {
  color: crimson;
}
<script src="https://unpkg.com/vue@next"></script>

<div id="app">
  <input 
    v-model="foo" 
    :pattern="pattern"
    @keypress="changeFoo" />

  <p v-if="invalid" class="error">You pressed a forbidden character!</p>
</div>

【讨论】:

  • 酷。很高兴它有帮助。 :-)
  • @tao 那我们来说说按钮,如何保证实际的点击动作不被篡改?可能性是无穷无尽的。毕竟,我们在这里谈论的是用户体验,不是吗?
  • 篡改客户端始终是可能的,因此必须处理常见情况,毕竟我的用户也可以直接输入我的输入元素,或者因为 chrome 扩展而无意中弄乱了视图。不可能处理所有案件。
  • 这就是为什么你永远不应该覆盖他们的输入。您应该只在无效时通知他们。你的责任到此为止。这就是我要说的。如果您想要一个完美的用户界面,请将您的用户视为国王。字面上地。一个皇室。在我看来,你不应该改变国王的输入。您应该乐于助人地指出它可能不适用于该值。这一切都是为了向用户展示尽可能多的尊重。
  • @tao 好吧,按照我的方法,我们显然没有覆盖任何东西——我们正在限制不需要的输入。关于“通知”您的用户,您是说当我们应该(至少尝试)阻止他们首先进入时,我们将允许在电话/信用卡字段上输入文本?我会说这是一个非常糟糕的用户体验。
【解决方案3】:

回答我自己的问题。

@Yom S 找到的第一个解决方案:正如我们在讨论中指出的那样,关键相关事件都将在 onInput 事件之前触发。然后,您可以在 keydown 事件上进行 preventDefault() 并将键值连接到当前字符串。我没有选择它,因为它很冗长,并且在我的突变和组件中的自定义方法之间分割了逻辑。

我选择的第二个解决方案。不覆盖默认行为,而是在我的突变中系统地覆盖它。所以,之前:

  if (target.checkValidity()) {
    state.foo = { name: target.value };
  }

变成

  if (target.checkValidity()) {
    state.foo = { name: value };
  } else {
    state.foo = { name: value.substring(0, value.length - 1) };
  }

通过每次 onInput 触发时始终改变状态,我确信默认输入行为将始终被覆盖。

注意:如果我必须在更新状态之前执行异步操作,我将选择第一个解决方案。

第三种解决方案,使用 setter 和 getter,请参阅@Tao 解决方案。与第二个解决方案相同,不推荐用于异步调用,但比防止按键默认值更简单。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-30
    相关资源
    最近更新 更多