7.實戰(zhàn) 2:組合多選框組件——CheckboxGroup & Checkbox

實戰(zhàn) 2:組合多選框組件——CheckboxGroup & Checkbox

在第 5 節(jié)省咨,我們完成了具有數(shù)據(jù)校驗功能的組件 Form,本小節(jié)繼續(xù)開發(fā)一個新的組件——組合多選框 Checkbox。它作為基礎(chǔ)組件执俩,也能集成在 Form 內(nèi)并應(yīng)用其驗證規(guī)則蚂维。

Checkbox 組件概覽

多選框組件也是由兩個組件組成:CheckboxGroup 和 Checkbox。單獨(dú)使用時毙籽,只需要一個 Checkbox洞斯,組合使用時,兩者都要用到坑赡。效果如下圖所示:

image

單獨(dú)使用烙如,常見的場景有注冊時勾選以同意注冊條款,它只有一個獨(dú)立的 Checkbox 組件毅否,并且綁定一個布爾值亚铁,示例如下:

<template>
  <i-checkbox v-model="single">單獨(dú)選項</i-checkbox>
</template>
<script>
  export default {
    data () {
      return {
        single: false
      }
    }
  }
</script>

而組合使用的場景就很多了,填寫表單時會經(jīng)常用到螟加,它的結(jié)構(gòu)如下:

<template>
  <i-checkbox-group v-model="multiple">
    <i-checkbox label="option1">選項 1</i-checkbox>
    <i-checkbox label="option2">選項 2</i-checkbox>
    <i-checkbox label="option3">選項 3</i-checkbox>
    <i-checkbox label="option4">選項 4</i-checkbox>
  </i-checkbox-group>
</template>
<script>
  export default {
    data () {
      return {
        multiple: ['option1', 'option3']
      }
    }
  }
</script>

v-model 用在了 CheckboxGroup 上徘溢,綁定的值為一個數(shù)組,數(shù)組的值就是內(nèi)部 Checkbox 綁定的 label捆探。

用法看起來比 Form 要簡單多然爆,不過也有兩個個技術(shù)難點:

  • Checkbox 要同時支持單獨(dú)使用和組合使用的場景;
  • CheckboxGroup 和 Checkbox 內(nèi)可能嵌套其它的布局組件黍图。

對于第一點曾雕,要在 Checkbox 初始化時判斷是否父級有 CheckboxGroup,如果有就是組合使用的助被,否則就是單獨(dú)使用剖张。而第二點,正好可以用上一節(jié)的通信方法揩环,很容易就能解決搔弄。

兩個組件并行開發(fā),會容易理不清邏輯丰滑,不妨我們先開發(fā)獨(dú)立的 Checkbox 組件肯污。

單獨(dú)使用的 Checkbox

設(shè)計一個組件時,還是要從它的 3 個 API 入手:prop吨枉、event蹦渣、slot。

因為要在 Checkbox 組件上直接使用 v-model 來雙向綁定數(shù)據(jù)貌亭,那必不可少的一個 prop 就是 value柬唯,還有 event input,因為 v-model 本質(zhì)上是一個語法糖(如果你還不清楚這種用法圃庭,可以閱讀最后的擴(kuò)展閱讀 1)锄奢。

理論上失晴,我們只需要給 value 設(shè)置為布爾值即可,也就是 true / false拘央,不過為了擴(kuò)展性涂屁,我們再定義兩個 props:trueValuefalseValue,它們允許用戶指定 value 用什么值來判斷是否選中灰伟。因為實際開發(fā)中拆又,數(shù)據(jù)庫中并不直接保存 true / false,而是 1 / 0 或其它字符串栏账,如果強(qiáng)制使用 Boolean帖族,使用者就要再額外轉(zhuǎn)換一次,這樣的 API 設(shè)計不太友好挡爵。

除此之外竖般,還需要一個 disabled 屬性來表示是否禁用。

自定義事件 events 上文已經(jīng)說了一個 input茶鹃,用于實現(xiàn) v-model 語法糖涣雕;另一個就是 on-change,當(dāng)選中 / 取消選中時觸發(fā)闭翩,用于通知父級狀態(tài)發(fā)生了變化挣郭。

slot 使用默認(rèn)的就好,顯示輔助文本男杈。

理清楚了 API丈屹,先來寫一個基礎(chǔ)的 v-model 功能调俘,這在大部分組件中都類似伶棒。

src/components 下新建目錄 checkbox,并新建兩個文件 checkbox.vuecheckbox-group.vue彩库。我們先來看 Checkbox:

<!-- checkbox.vue -->
<template>
  <label>
    <span>
      <input
             type="checkbox"
             :disabled="disabled"
             :checked="currentValue"
             @change="change">
    </span>
    <slot></slot>
  </label>
</template>
<script>
  export default {
    name: 'iCheckbox',
    props: {
      disabled: {
        type: Boolean,
        default: false
      },
      value: {
        type: [String, Number, Boolean],
        default: false
      },
      trueValue: {
        type: [String, Number, Boolean],
        default: true
      },
      falseValue: {
        type: [String, Number, Boolean],
        default: false
      }
    },
    data () {
      return {
        currentValue: this.value
      };
    },
    methods: {
      change (event) {
        if (this.disabled) {
          return false;
        }

        const checked = event.target.checked;
        this.currentValue = checked;

        const value = checked ? this.trueValue : this.falseValue;
        this.$emit('input', value);
        this.$emit('on-change', value);
      }
    }
  }
</script>

因為 value 被定義為 prop肤无,它只能由父級修改,本身是不能修改的骇钦,在 <input> 觸發(fā) change 事件宛渐,也就是點擊選擇時,不能由 Checkbox 來修改這個 value眯搭,所以我們在 data 里定義了一個 currentValue窥翩,并把它綁定在 <input :checked="currentValue">,這樣就可以在 Checkbox 內(nèi)修改 currentValue鳞仙。這是自定義組件使用 v-model 的“慣用伎倆”寇蚊。

代碼看起來都很簡單,但有三個細(xì)節(jié)需要額外說明:

  1. 選中的控件棍好,直接使用了 <input type="checkbox">仗岸,而沒有用 div + css 來自己實現(xiàn)選擇的邏輯和樣式允耿,這樣的好處是,使用 input 元素扒怖,你的自定義組件仍然為 html 內(nèi)置的基礎(chǔ)組件较锡,可以使用瀏覽器默認(rèn)的行為和快捷鍵,也就是說盗痒,瀏覽器知道這是一個選擇框蚂蕴,而換成 div + css,瀏覽器可不知道這是個什么鬼积糯。如果你覺得原生的 input 丑掂墓,沒關(guān)系,是可以用 css 美化的看成,不過這不是本小冊的重點君编,在此就不介紹了。
  2. <input>川慌、<slot> 都是包裹在一個 <label> 元素內(nèi)的吃嘿,這樣做的好處是,當(dāng)點擊 <slot> 里的文字時梦重,<input> 選框也會被觸發(fā)兑燥,否則只有點擊那個小框才會觸發(fā),那樣不太容易選中琴拧,影響用戶體驗降瞳。
  3. currentValue 仍然是布爾值(true / false),因為它是組件 Checkbox 自己使用的蚓胸,對于使用者無需關(guān)心挣饥,而 value 可以是 String、Number 或 Boolean沛膳,這取決于 trueValuefalseValue 的定義扔枫。

現(xiàn)在實現(xiàn)的 v-model,只是由內(nèi)而外的,也就是說,通過點擊 <input>選擇浦夷,會通知到使用者徙瓶,而使用者手動修改了 prop value ,Checkbox 是沒有做響應(yīng)的,那繼續(xù)補(bǔ)充代碼:

<!-- checkbox.vue,部分代碼省略 -->
<script>
  export default {
    watch: {
      value (val) {
        if (val === this.trueValue || val === this.falseValue) {
          this.updateModel();
        } else {
          throw 'Value should be trueValue or falseValue.';
        }
      }
    },
    methods: {
      updateModel () {
        this.currentValue = this.value === this.trueValue;
      }
    }
  }
</script>

我們對 prop value 使用 watch 進(jìn)行了監(jiān)聽,當(dāng)父級修改它時糠排,會調(diào)用 updateModel 方法,同步修改內(nèi)部的 currentValue 泊交。不過乳讥,不是所有的值父級都能修改的柱查,所以用 if 條件判斷了父級修改的值是否符合 trueValue / falseValue 所設(shè)置的,否則會拋錯云石。

Checkbox 也是一個基礎(chǔ)的表單類組件唉工,它完全可以集成到 Form 里,所以汹忠,我們使用 Emitter 在 change 事件觸發(fā)時淋硝,向 Form 派發(fā)一個事件,這樣你就可以用第 5 節(jié)的 Form 組件來做數(shù)據(jù)校驗了:

<!-- checkbox.vue宽菜,部分代碼省略 -->
<script>
  import Emitter from '../../mixins/emitter.js';

  export default {
    mixins: [ Emitter ],
    methods: {
      change (event) {
        // ... 
        this.$emit('input', value);
        this.$emit('on-change', value);
        this.dispatch('iFormItem', 'on-form-change', value);
      }
    },
  }
</script>

至此谣膳,Checkbox 已經(jīng)可以單獨(dú)使用了,并支持 Form 的數(shù)據(jù)校驗铅乡。下面來看組合使用继谚。

組合使用的 CheckboxGroup

友情提示:請先閱讀 Vue.js 文檔的 https://cn.vuejs.org/v2/guide/forms.html#復(fù)選框 內(nèi)容。

CheckboxGroup 的 API 很簡單:

  • props:value阵幸,與 Checkbox 的類似花履,用于 v-model 雙向綁定數(shù)據(jù),格式為數(shù)組挚赊;
  • events:on-change诡壁,同 Checkbox;
  • slots:默認(rèn)荠割,用于放置 Checkbox妹卿。

如果寫了 CheckboxGroup,那就代表你要組合使用多選框蔑鹦,而非單獨(dú)使用夺克,兩種模式,只能用其一举反,而判斷的依據(jù)懊直,就是是否用了 CheckboxGroup 組件扒吁。所以在 Checkbox 組件內(nèi)火鼻,我們用上一節(jié)的 findComponentUpward 方法判斷父組件是否有 CheckboxGroup

<!-- checkbox.vue,部分代碼省略 -->
<template>
  <label>
    <span>
      <input
             v-if="group"
             type="checkbox"
             :disabled="disabled"
             :value="label"
             v-model="model"
             @change="change">
      <input
             v-else
             type="checkbox"
             :disabled="disabled"
             :checked="currentValue"
             @change="change">
    </span>
    <slot></slot>
  </label>
</template>
<script>
  import { findComponentUpward } from '../../utils/assist.js';

  export default {
    name: 'iCheckbox',
    props: {
      label: {
        type: [String, Number, Boolean]
      }
    },
    data () {
      return {
        model: [],
        group: false,
        parent: null
      };
    },
    mounted () {
      this.parent = findComponentUpward(this, 'iCheckboxGroup');

      if (this.parent) {
        this.group = true;
      }

      if (this.group) {
        this.parent.updateModel(true);
      } else {
        this.updateModel();
      }
    },
  }
</script>

在 mounted 時雕崩,通過 findComponentUpward 方法魁索,來判斷父級是否有 CheckboxGroup 組件,如果有盼铁,就將 group 置為 true粗蔚,并觸發(fā) CheckboxGroup 的 updateModel 方法,下文會介紹它的作用饶火。

在 template 里鹏控,我們又寫了一個 <input> 來區(qū)分是否是 group 模式致扯。Checkbox 的 data 里新增加的 model 數(shù)據(jù),其實就是父級 CheckboxGroup 的 value当辐,會在下文的 updateModel 方法里給 Checkbox 賦值抖僵。

Checkbox 新增的 prop: label 只會在組合使用時有效,結(jié)合 model 來使用缘揪,用法已在 Vue.js 文檔中介紹了 https://cn.vuejs.org/v2/guide/forms.html#復(fù)選框耍群。

在組合模式下,Checkbox 選中找筝,就不用對 Form 派發(fā)事件了蹈垢,應(yīng)該在 CheckboxGroup 中派發(fā),所以對 Checkbox 做最后的修改:

<!-- checkbox.vue袖裕,部分代碼省略 -->
<script>
  export default {
    methods: {
      change (event) {
        if (this.disabled) {
          return false;
        }

        const checked = event.target.checked;
        this.currentValue = checked;

        const value = checked ? this.trueValue : this.falseValue;
        this.$emit('input', value);

        if (this.group) {
          this.parent.change(this.model);
        } else {
          this.$emit('on-change', value);
          this.dispatch('iFormItem', 'on-form-change', value);
        }
      },
      updateModel () {
        this.currentValue = this.value === this.trueValue;
      },
    },
  }
</script>

剩余的工作曹抬,就是完成 checkbox-gourp.vue 文件:

<!-- checkbox-group.vue -->
<template>
  <div>
    <slot></slot>
  </div>
</template>
<script>
  import { findComponentsDownward } from '../../utils/assist.js';
  import Emitter from '../../mixins/emitter.js';

  export default {
    name: 'iCheckboxGroup',
    mixins: [ Emitter ],
    props: {
      value: {
        type: Array,
        default () {
          return [];
        }
      }
    },
    data () {
      return {
        currentValue: this.value,
        childrens: []
      };
    },
    methods: {
      updateModel (update) {
        this.childrens = findComponentsDownward(this, 'iCheckbox');
        if (this.childrens) {
          const { value } = this;
          this.childrens.forEach(child => {
            child.model = value;

            if (update) {
              child.currentValue = value.indexOf(child.label) >= 0;
              child.group = true;
            }
          });
        }
      },
      change (data) {
        this.currentValue = data;
        this.$emit('input', data);
        this.$emit('on-change', data);
        this.dispatch('iFormItem', 'on-form-change', data);
      }
    },
    mounted () {
      this.updateModel(true);
    },
    watch: {
      value () {
        this.updateModel(true);
      }
    }
  };
</script>


代碼很容易理解,需要介紹的就是 updateModel 方法急鳄°宓唬可以看到,一共有 3 個地方調(diào)用了 updateModel攒岛,其中兩個是 CheckboxGroup 的 mounted 初始化和 watch 監(jiān)聽的 value 變化時調(diào)用赖临;另一個是在 Checkbox 里的 mounted 初始化時調(diào)用。這個方法的作用就是在 CheckboxGroup 里通過 findComponentsDownward 方法找到所有的 Checkbox灾锯,然后把 CheckboxGroup 的 value兢榨,賦值給 Checkbox 的 model,并根據(jù) Checkbox 的 label顺饮,設(shè)置一次當(dāng)前 Checkbox 的選中狀態(tài)吵聪。這樣無論是由內(nèi)而外選擇,或由外向內(nèi)修改數(shù)據(jù)兼雄,都是雙向綁定的吟逝,而且支持動態(tài)增加 Checkbox 的數(shù)量。

以上就是組合多選組件——CheckboxGroup & Checkbox 的全部內(nèi)容赦肋,不知道你是否 get 到了呢块攒!

留兩個小作業(yè):

  1. 將 CheckboxGroup 和 Checkbox 組件集成在 Form 里完成一個數(shù)據(jù)校驗的示例;
  2. 參考本節(jié)的代碼佃乘,實現(xiàn)一個單選組件 Radio 和 RadioGroup囱井。

結(jié)語

你看到的簡單組件,其實都不簡單趣避。

擴(kuò)展閱讀

注:本節(jié)部分代碼參考 iView庞呕。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子住练,更是在濱河造成了極大的恐慌地啰,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件讲逛,死亡現(xiàn)場離奇詭異髓绽,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)妆绞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門顺呕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人括饶,你說我怎么就攤上這事株茶。” “怎么了图焰?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵启盛,是天一觀的道長。 經(jīng)常有香客問我技羔,道長僵闯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任藤滥,我火速辦了婚禮鳖粟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘拙绊。我一直安慰自己向图,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布标沪。 她就那樣靜靜地躺著榄攀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪金句。 梳的紋絲不亂的頭發(fā)上檩赢,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機(jī)與錄音违寞,去河邊找鬼贞瞒。 笑死,一個胖子當(dāng)著我的面吹牛坞靶,可吹牛的內(nèi)容都是我干的憔狞。 我是一名探鬼主播蝴悉,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼彰阴,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了拍冠?” 一聲冷哼從身側(cè)響起尿这,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤簇抵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后射众,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體碟摆,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年叨橱,在試婚紗的時候發(fā)現(xiàn)自己被綠了典蜕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡罗洗,死狀恐怖愉舔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情伙菜,我是刑警寧澤轩缤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站贩绕,受9級特大地震影響火的,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜淑倾,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一馏鹤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧娇哆,春花似錦假瞬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至垄开,卻和暖如春琴许,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背溉躲。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工榜田, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人锻梳。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓箭券,卻偏偏與公主長得像,于是被迫代替她去往敵國和親疑枯。 傳聞我的和親對象是個殘疾皇子辩块,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容