elementui原始碼學習之仿寫一個el-button

水冗水孚發表於2022-12-28

本篇文章記錄仿寫一個el-button元件細節,從而有助於大家更好理解餓了麼ui對應元件具體工作細節。本文是elementui原始碼學習仿寫系列的又一篇文章,後續空閒了會不斷更新並仿寫其他元件。原始碼在github上,大家可以拉下來,npm start執行跑起來,結合註釋有助於更好的理解

網站效果演示:http://ashuai.work:8888/#/myB...

GitHub倉庫地址:https://github.com/shuirongsh...

什麼是Button元件

按鈕用於點選,一般是做事件的響應。

按鈕封裝效果圖

按鈕分類

  • 單一按鈕

    • 預設按鈕
    • 主題按鈕(primary、success、warning、error)
    • 按鈕大小(small、middle、big)
    • 按鈕禁用(disabled)
    • 按鈕載入(loading)
    • 按鈕的圖示位置(預設圖示在按鈕文字左側)
    • 圖示按鈕(沒有按鈕文字)
    • 單一文字按鈕
  • 按鈕組(按鈕組中有多個按鈕)

預設按鈕

預設按鈕很簡單,只是寫一個最普通的樣式即可

<button :class="[ 'myButton' ]" />

.myButton {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  box-sizing: border-box;
  padding: 12px 16px;
  background-color: rgba(0, 0, 0, 0.1);
  color: #222;
  border: none;
  cursor: pointer;
  user-select: none; // 不讓選中文字
  transition: all 0.3s;
  font-size: 14px;
}
// 懸浮效果
.myButton:hover {
  background-color: rgba(0, 0, 0, 0.2);
}
// 按中效果
.myButton:active {
  background-color: rgba(0, 0, 0, 0.3);
}

筆者這裡是將懸浮的效果和按中的效果,設定背景色越來越深。這樣的話,看著效果比較明顯

主題按鈕

所謂按鈕的主題,就是新增不同的類名,比如primary主題的按鈕,就加上.primary類名、success主題的按鈕,就加上.success類名。然後使用動態class去新增即可(這裡使用動態class的陣列用法)。如:

<button :class="[ 'myButton', type ]" />

變數type的值源自於使用按鈕元件時,傳遞進來的type引數

const typeArr = [
  "",
  "primary",
  "success",
  "warning",
  "error",
  "text",
  "dangerText",
];

props:{
    type: { // 按鈕主題型別
      type: String,
      validator(val) {
        return typeArr.includes(val); // 這裡可以加一個校驗函式,其實不加也行
      },
    },
}

然後給不同type值加上對應的樣式即可。如下:

// primary樣式
.primary {
  background-color: #1867c0;
  color: #fff;
}
.primary:hover {
  background-color: #0854ac;
}
.primary:active {
  background-color: #033d7f;
}

// success樣式
.success {
  background-color: #19be6b;
  color: #fff;
}
.success:hover {
  background-color: #0ea459;
}
.success:active {
  background-color: #008140;
}

// warning樣式
.warning {
  background-color: #ffc163;
  color: #fff;
}
.warning:hover {
  background-color: #db952d;
}
.warning:active {
  background-color: #b97b1d;
}

// 等等type值樣式...

按鈕大小

按鈕大小可以使用padding值的大小去控制,也可以直接使用zoom縮放做控制

這裡使用動態style搭配計算屬性的方式去控制,如下程式碼:

// 不同的大小指定不同的縮放程度
const sizeObj = {
  small: 0.85,
  middle: 1,
  big: 1.2,
};

props:{ size: String }

<button :style="styleCal" />

computed: {
    styleCal() {
        return {
            zoom: sizeObj[this.size] // zoom縮放的值大小取決於傳遞進來的size值
        }
    }
}

按鈕禁用

按鈕禁用disable沒啥好說的,主要是要注意loading的時候,也要禁用掉,loading載入的時候,不允許使用者再點選。

<button :disabled="disabled || loading" />

props:{
    loading:Boolean
}

這裡注意一下,按鈕禁用的樣式也是透過動態class加上的,請往下看

按鈕載入

注意載入時樣式和載入按鈕圖示出來的時候,將其他的圖示給隱藏起來。(同一時刻,只能有一個按鈕圖示,這樣保證按鈕載入時簡潔一些)

  <button
    :class="[
      'myButton', // 預設樣式
      disabled ? 'disabledBtn' : '', // 動態加上禁用按鈕樣式
      loading ? 'loadingBtn' : '', // 動態加上loading載入中按鈕樣式
      type, // 主題樣式
    ]"
    :disabled="disabled || loading" // 禁用時禁用,載入時也禁用
  >
    <i class="el-icon-loading" v-if="loading"></i>
    <!-- 使用傳進來的圖示,透過動態style控制圖示和文字見的間隔,同一時刻下,
    只能有一個圖示出現,所以有loading圖示了,就不能有別的圖示了 -->
    <i :class="icon" :style="styleGap" v-if="icon && !loading"></i>
    <slot></slot>
  </button>

按鈕的圖示位置

預設從左往右排列(圖示在左側、文字在右側),這裡我們可以使用彈性盒的方向flexDirection屬性,來控制從左往右還是從右往左排列

<button :style="styleCal"/>

styleCal() {
  // 控制縮放和指定預設圓角以及設定圖示在文字左側還是右側
  let styleObj = {
    zoom: sizeObj[this.size],
    borderRadius: "5px",
    flexDirection: this.rightIcon ? "row-reverse" : "row",
  };
  return styleObj;
},

圖示按鈕和單一文字按鈕

這兩個也很簡單,

  • 圖示按鈕注意加圓角的時機
  • 單一文字按鈕的樣式要預留設定一份即可

然後動態控制一下即可

按鈕組

按鈕組注意事項:

  • 首先將所有的按鈕的圓角全部去掉(這樣的話,所有的按鈕都是方方正正的按鈕了)
  • 然後單獨給第一個按鈕:first-of-type的左上角和左下角的圓角設定一下
  • 然後再給最後一個按鈕last-of-type的右上角和右下角的圓角設定一下
  • 最後,按鈕組之間需要有間隔,這裡使用border-right做分割線
  • 最最後,再把最後一個按鈕的右邊框去掉即可,如下css程式碼
// 附上按鈕組樣式
.myButtonGroup > .myButton {
  border-radius: unset !important; // 給所有的按鈕都去掉圓角
  border-right: 1px solid #fff; // 給按鈕加上分隔線條
}
// 第一個按鈕左側圓角
.myButtonGroup > .myButton:first-of-type {
  border-top-left-radius: 5px !important; 
  border-bottom-left-radius: 5px !important;
}
// 最後一個按鈕的右側圓角
.myButtonGroup > .myButton:last-of-type {
  border-top-right-radius: 5px !important;
  border-bottom-right-radius: 5px !important;
  border-right: none; // 同時,清除最後一個按鈕的右側邊框
}

程式碼

複製貼上即可使用,如果道友覺得程式碼幫忙到了您,歡迎給我們github倉庫一個star哈?

myButton元件

<template>
  <button
    :style="styleCal"
    :class="[
      'myButton',
      disabled ? 'disabledBtn' : '',
      loading ? 'loadingBtn' : '',
      type,
    ]"
    :disabled="disabled || loading"
    @click="clickButton"
  >
    <i class="el-icon-loading iii" v-if="loading"></i>
    <!-- 使用傳進來的圖示,透過動態style控制圖示和文字見的間隔,同一時刻下,
    只能有一個圖示出現,所以有loading圖示了,就不能有別的圖示了 -->
    <i :class="icon" :style="styleGap" v-if="icon && !loading"></i>
    <!-- 普通插槽有東西才去渲染 -->
    <span v-if="$slots.default"><slot></slot></span>
  </button>
</template>

<script>
// 型別校驗
const typeArr = [
  "",
  "primary",
  "success",
  "warning",
  "error",
  "text",
  "dangerText",
];
const sizeArr = ["", "small", "middle", "big"]; // 大小檢驗
const sizeObj = {
  // 不同的大小指定不同的縮放程度
  small: 0.85,
  middle: 1,
  big: 1.2,
};
export default {
  name: "myButton",
  props: {
    disabled: Boolean,
    loading: Boolean, // loading時,不可繼續點選(繼續點選不生效)
    rightIcon: Boolean, // 透過彈性盒的方向控制圖示的位置
    type: {
      type: String,
      validator(val) {
        return typeArr.includes(val);
      },
    },
    size: {
      type: String,
      validator(val) {
        return sizeArr.includes(val);
      },
    },
    icon: String,
  },
  computed: {
    styleCal() {
      // 控制縮放和指定預設圓角以及設定圖示在文字左側還是右側
      let styleObj = {
        zoom: sizeObj[this.size],
        borderRadius: "5px",
        flexDirection: this.rightIcon ? "row-reverse" : "row",
      };
      // 當有圖示,且沒有文字的時候(或預設插槽沒傳),就讓按鈕變成圓形按鈕
      if ((this.icon && !this.$slots.default) || !this.$slots.default[0].text) {
        styleObj["borderRadius"] = "50%";
        styleObj["padding"] = "12px";
      }
      return styleObj;
    },
    styleGap() {
      // 有圖示,有文字,圖示在左側
      if (
        (this.icon && !this.$slots.default) ||
        (this.$slots.default[0].text && !this.rightIcon)
      ) {
        return {
          paddingRight: "1px",
        };
      }
      // 有圖示,有文字,圖示在右側
      if (
        (this.icon && !this.$slots.default) ||
        (this.$slots.default[0].text && this.rightIcon)
      ) {
        return {
          paddingLeft: "1px",
        };
      }
    },
  },
  methods: {
    clickButton(e) {
      if (this.disabled) return;
      this.$emit("click", e); // 傳出去,便於使用
    },
  },
};
</script>

<style lang='less' scoped>
/* 關於按鈕的樣式即寫好幾套樣式,然後透過型別等各種引數去控制樣式,最終實現對應效果 */

// 基礎樣式
.myButton {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  box-sizing: border-box;
  padding: 12px 16px;
  background-color: rgba(0, 0, 0, 0.1);
  color: #222;
  border: none;
  cursor: pointer;
  user-select: none;
  transition: all 0.3s;
  font-size: 14px;
  .iii {
    margin-right: 4px;
  }
}
.myButton:hover {
  background-color: rgba(0, 0, 0, 0.2);
}
.myButton:active {
  background-color: rgba(0, 0, 0, 0.3);
}

// primary樣式
.primary {
  background-color: #1867c0;
  color: #fff;
}
.primary:hover {
  background-color: #0854ac;
}
.primary:active {
  background-color: #033d7f;
}

// success樣式
.success {
  background-color: #19be6b;
  color: #fff;
}
.success:hover {
  background-color: #0ea459;
}
.success:active {
  background-color: #008140;
}

// warning樣式
.warning {
  background-color: #ffc163;
  color: #fff;
}
.warning:hover {
  background-color: #db952d;
}
.warning:active {
  background-color: #b97b1d;
}

// error樣式
.error {
  background-color: #ff5252;
  color: #fff;
}
.error:hover {
  background-color: #fd3030;
}
.error:active {
  background-color: #d50000;
}

// text樣式
.text {
  background-color: unset;
  color: #409eff;
  padding: 2px 4px;
}
.text:hover {
  background-color: unset;
  opacity: 0.9;
}
.text:active {
  background-color: unset;
  opacity: 1;
  color: #1a7ada;
}

// dangerText樣式
.dangerText {
  background-color: unset;
  color: #ff5252;
  padding: 2px 4px;
}
.dangerText:hover {
  background-color: unset;
  opacity: 0.9;
}
.dangerText:active {
  background-color: unset;
  opacity: 1;
  color: #d50000;
}

// 載入按鈕樣式
.loadingBtn {
  opacity: 0.6;
  pointer-events: none; // 值為none就沒有hover和active效果了
}

// disabled樣式(注意樣式的順序)
.disabledBtn {
  background-color: rgba(0, 0, 0, 0.12);
  color: #bbb;
}
.disabledBtn:hover {
  opacity: 1;
  cursor: not-allowed;
  background-color: rgba(0, 0, 0, 0.12);
}
.disabledBtn:active {
  color: #bbb;
  opacity: 1;
  background-color: rgba(0, 0, 0, 0.12);
}

// 附上按鈕組樣式
.myButtonGroup > .myButton {
  border-radius: unset !important;
  border-right: 1px solid #fff;
}
.myButtonGroup > .myButton:first-of-type {
  border-top-left-radius: 5px !important;
  border-bottom-left-radius: 5px !important;
}
.myButtonGroup > .myButton:last-of-type {
  border-top-right-radius: 5px !important;
  border-bottom-right-radius: 5px !important;
  border-right: none;
}
</style>

myButtonGroup元件

<template>
  <div class="myButtonGroup">
    <slot></slot>
  </div>
</template>
<script>
export default {
  name: "myButtonGroup",
};
</script>
<style>
.myButtonGroup {
  display: inline-flex !important;
  align-items: center;
}
</style>

使用的時候

<template>
  <div>
    <h5>單個按鈕</h5>
    <br />
    <button @click="clickLoad">載入切換</button>
    <div class="btnBox">
      <span class="btn" v-for="(item, index) of btnArr">
        <my-button
          style="margin-right: 16px"
          :key="index"
          :type="item.type"
          :size="item.size"
          :disabled="item.disabled"
          :loading="item.loading"
          :icon="item.icon"
          :rightIcon="item.rightIcon"
          @click="
            (e) => {
              clickBtn(item, e);
            }
          "
          >{{ item.name }}</my-button
        >
      </span>
    </div>
    <br />
    <h5>按鈕組</h5>
    <br />
    <my-button-group>
      <my-button type="success" icon="el-icon-arrow-left">上一頁</my-button>
      <my-button type="success" icon="el-icon-arrow-right" :rightIcon="true"
        >下一頁</my-button
      >
    </my-button-group>
    <br />
    <br />
    <my-button-group>
      <my-button type="primary" icon="el-icon-user"></my-button>
      <my-button type="primary" icon="el-icon-view"></my-button>
      <my-button type="primary" icon="el-icon-star-off"></my-button>
      <my-button type="primary" icon="el-icon-chat-dot-square"></my-button>
      <my-button type="primary" icon="el-icon-share"></my-button>
    </my-button-group>
  </div>
</template>

<script>
export default {
  name: "myButtonName",
  data() {
    return {
      loadingF: false,
      btnArr: [
        {
          type: "",
          name: "預設按鈕",
        },
        {
          type: "primary",
          name: "primary",
        },
        {
          type: "success",
          name: "success",
        },
        {
          type: "warning",
          name: "warning",
        },
        {
          type: "error",
          name: "error",
        },
        {
          type: "primary",
          name: "size=small",
          size: "small",
        },
        {
          type: "primary",
          name: "size=middle",
          size: "middle",
        },
        {
          type: "primary",
          name: "size=big",
          size: "big",
        },
        {
          type: "success", // 不管type什麼型別,只要禁用全部置灰
          name: "disabled",
          disabled: true,
        },
        {
          type: "primary",
          name: "等待載入",
          loading: false,
        },
        {
          type: "success",
          name: "等待載入",
          loading: false,
        },
        {
          type: "success",
          name: "icon",
          icon: "el-icon-star-on",
        },
        {
          type: "success",
          name: "icon",
          icon: "el-icon-star-on",
          rightIcon: true,
        },
        {
          type: "success",
          name: "",
          icon: "el-icon-edit",
        },
        {
          type: "error",
          name: "",
          icon: "el-icon-delete",
        },
        {
          type: "text",
          name: "純text按鈕",
          // loading: true,
        },
        {
          type: "dangerText",
          name: "dangerText按鈕",
          icon: "el-icon-delete-solid",
        },
        {
          type: "text",
          name: "text禁用",
          disabled: true,
        },
      ],
    };
  },
  methods: {
    clickLoad() {
      let lebel = this.btnArr[9].name;
      let newItem9 = {
        type: "primary",
        name: lebel == "等待載入" ? "載入中" : "等待載入",
        loading: lebel == "等待載入" ? true : false,
      };
      this.$set(this.btnArr, 9, newItem9);
      let newItem10 = {
        type: "success",
        name: lebel == "等待載入" ? "載入中" : "等待載入",
        loading: lebel == "等待載入" ? true : false,
      };
      this.$set(this.btnArr, 10, newItem10);
    },
    // 注意這種寫法,可接收多個引數
    clickBtn(item, e) {
      console.log("clickBtn", item, e);
    },
  },
};
</script>

<style>
.btnBox {
  width: 100%;
  box-sizing: border-box;
  padding: 24px 0;
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
}
.btn {
  margin-bottom: 24px;
}
</style>
A good memory is better than a bad pen. Write it down...

相關文章