表单处理 Object 里的 Array

今天来看看一个常见问题。

{
  first_name: 'chris',
  last_name: 'wang',
  email: '[email protected]',
  camp: {
    name: 'web camp',
    member_count: 10
  },
  skills: ['javascript', 'html', 'css']
}

先做已经会的

先依昨天讲的 UserForm 的 component 可以这样写。

<form @submit.prevent="$emit('submit')">
  <label>firstName<br/>
    <input type="text"
      :disabled="!$listeners['update:firstName']"
      :value="data.firstName"
      @input="$emit('update:firstName', {
        ...data,
        firstName: $event.target.value
      })">
  </label><br/>
  <label>lastName<br/>
    <input type="text"
      :disabled="!$listeners['update:lastName']"
      :value="data.lastName"
      @input="$emit('update:lastName', {
        ...data,
        lastName: $event.target.value
      })">
  </label><br/>
  <label>email<br/>
    <input type="email"
      :disabled="!$listeners['update:email']"
      :value="data.email"
      @input="$emit('update:email', {
        ...data,
        email: $event.target.value
      })">
  </label><br/>
  <CampForm
    :data="data.camp"
    @update:name="$emit('update:camp', {
      ...data,
      camp: $event
    })"
    @update:member_count="$emit('update:camp', {
      ...data,
      camp: $event
    })"
  ></CampForm>
  <pre>skills: {{data.skills}}</pre>
  <input type="submit" value="送出">
</form>

画面

还有一个 skills 怎麽办呢?

{
  // ....
  skills: ['javascript', 'html', 'css']
}

这次是 Array 要怎麽看待呢?

除了使用 radio 这样有 options 的选择元件之外,如果是 tags 怎办呢?

其实,「就是 Array 的操作要怎麽做在 component 上面」。
以这个概念回溯回去。

  • 一般型别变 input ,就是将一般型别的操作,做到 (原生的) component 上面
  • Object 变表单,就是将物件的操作,做到 component
  • 依此类推,Array 也一样

也因为需求不同,需要用到的操作自然也不会不同。
今天,我们来试看看 tags 的做法

目标画面

  • 可以显示一个资料列表
  • 可以新增一个空白栏位
  • 可以编辑任一已存在的栏位
  • 可以删除掉任一笔已存在的栏位

分别就是 CRUD (create, read, update, delete)

<div class="tags">
  <div class="tags">
    skills: <button @click.prevent="$emit('create', [...data, ''])">+</button><br />
    <ul>
      <li :key="index" v-for="(tag, index) in data">
        <input
          :id="`input-tag-${index}`"
          type="text"
          :value="tag"
          @input="$emit('update', [
            ...data.slice(0, index),
            $event.target.value,
            ...data.slice(index+1),
          ])"
        >
        <button @click.prevent="$emit('delete', [
          ...data.slice(0, index),
          ...data.slice(index+1),
        ])">-</button>
      </li>
    </ul>
  </div>
</div>

在 UserForm 这一层,要使用 Tabs 来 CRUD skills

    <Tags
      :data="data.skills"
      @create="$emit('update:skills', {
        ...data,
        skills: $event
      })"
      @update="$emit('update:skills', {
        ...data,
        skills: $event
      })"
      @delete="$emit('update:skills', {
        ...data,
        skills: $event
      })"
    ></Tags>

使用 UserForm
加一个 @update:skills="$store.commit('user', $event)"
表示可以更新 skills

  <UserForm
    :data="data"
    @update:first_name="$store.commit('user', $event)"
    @update:last_name="$store.commit('user', $event)"
    @update:email="$store.commit('user', $event)"
    @update:camp="$store.commit('user', $event)"
    @update:skills="$store.commit('user', $event)"
    @submit="onSubmit"
  ></UserForm>

刚刚那个 ...data.slice() 是什麽巫术?

对於更新资料,坚持使用 immutable 的方式更新。
并且在触发的位置就决定如何组资料,这是最适合的。

对於 Array 的 immutable ,要靠 Array#slice 取得新的 sub array。
并且将新的值 (''),更新的值 ($event.target.value),甚至是删除值都可以做到

下面我们把「组 Array」和「往外传」分成两个步骤写出来。

create

var new_data = [
  ...data,
  ''
]
$emit('create', new_data)"

update

var new_data = [
  ...data.slice(0, index),
  new_item, // data[index] 的位置,要修改
  ...data.slice(index+1),
]
$emit('update', new_data)"

delete

var new_data = [
  ...data.slice(0, index),
  // data[index] 的位置,要删掉
  ...data.slice(index+1),
]
$emit('delete', new_data)

为型别操作而生!才是 web component 设计的思维

将型别的的操作,延伸到 component

输入

  • 简单型别,使用了 :value 将值输入 component
  • 复杂型别,使用了 :data 将物件输入 component

要这样分也是可以唷
物件型别,使用了 :data 将物件输入 component
阵列型别,使用了 :list 将物件输入 component

输出

  • 简单型别,使用了 @input 表示更新这个值,并将值输出 component
  • 物件型别,使用了 @update:property 表示物件更新某个属性,并将物件输入 component
  • 阵列型别,使用了 @create, @update, @delete 表示阵列更新某个元素,并将阵列输入 component

所以

在 input 的输入中,:value="data.firstName" 输出就是取得 firstName 的新值。
在 camp-form 的输入中 :data="data.camp" 输出就是取得 user.camp 的新值。
在 tags 的输入中 :data="data.skills" 输出就是取得 user.skills 的新值。

物件里的简单型别、物件里的物件型别、物件里的阵列型别,三件事情的更新层次一致之後,按照这样的观念实作,任何巢状式的物件,就真的不用害怕它的表单有多复杂了。

对应方式也许不是一对一,但是可以限缩在一个合理的有限范围之内,像是表单控制项与资料型别之间的关系,也有着一定的合理范围之内。

复习昨天的重点

有了昨天的学习心得。今天做出这样的结果是不是就快速许多了呢?

  • 当作是 input 的 v-model 的概念一样,要有进有出,做成 pure component。
  • 什麽型别进,就什麽型别出,不要有悬念,除非遇到更新照片这种特别的情况。
  • 有多少栏位,就做多少的 event 不要多也不要少,除两个栏位指的是同一件事,才可以共用 event
  • 尽可能的不要加入多余的 props,只需要传入物件。

之後是不是遇到什麽 JSON 都可以顺利的建立出它相对应的表单了呢?

回到 v-model 的写与不写

连续三天的 component 的介绍,到今天如果都了解的话,我想说说这一切的开始「舍弃 v-model」这件事,认真说起来我不写 v-model 很久了,让我保持读写分离的写法,也让我可以思考设计 component 的写法有更多的灵活与弹性。

v-model 本身代表的是 Vue.js 的 directives 厉害之处的一种表现,这是不用怀疑的,只是让我发现这样写让我觉得问题可以拆成一致的视角,在 Vue.js 里进行管理复杂度,不会因为资料复杂而让画面处理更复杂,感到非常的开心,为了可以交待这一切的思想源头,特别强调了不要写 v-model 这件事,但并不代表它不好,只是它不处在这些写法演进的脉胳之中。

到目前,希望有让你对 Vue.js 的全新视野与感受,写起来可以尝试更多的可能性。
也希望也可以透过回馈,了解其它的人在 Vue.js 上面的造脂与领悟。

咦?30 天还没到!明天要写什麽?!


<<:  DAY28 CNN(卷积神经网路 续二)

>>:  Day27 MANO开源专案使用之OSM-建立篇

远距工作停看听:挑战篇

前言 昨天分享了远距工作的好处,今天紧接着来看它带来的挑战,以及我们有什麽方式可以去改善它。 远距工...

Day 5: 人工智慧在音乐领域的应用 (AI发展史与简介 - 第二次寒冬)

今天我们接续昨日的话题,继续来聊聊AI发展史上的第二次寒冬。 前面提到,AI在1956年达特茅斯会议...

【Day 28】Hook 08:useCallback

useCallback 如果父元件所传递的 props 包含 Object, 则在元件因状态改变而 ...

Day13 - 画布操作与编制复杂图形3

创建多张画布 如果要有遮色效果或者两个图层不想互相被干预可以考虑增加内部画布去让自己操作比较能够好处...

Day14-Kubernetes 那些事 - Deployment 与 ReplicaSet(二)

前言 昨天的文章介绍了 Deployment 以及 ReplicaSet 的基本介绍後,接下来要介绍...