Vue3造轮子:Button组件

177 阅读2分钟

Button组件

需求分析

  • 可以有不同的等级(level)

  • 可以是链接,可以是文字

  • 可以click、focus、鼠标悬浮

  • 可以改变size: 大中小

  • 可以禁用(disabled)

  • 可以加载中(loading)

API设计

  • Button组件怎么用
<Button
  @click=?
  @focus=?
  @mouseover=?
  theme="button or link or text"
  level="main or normal or minor"
  size="big normal small"
  disabled
  loading
></Button>

Vue 3 属性绑定

让Button支持事件(@click @focus @mouseover)

在使用组件的时候,传入的事件,会默认传给组件的最外层的标签上,例如:如果button组件的最外层是<button><button>标签,那么传入的事件就会传给button标签。如果button组件是<div> <button> </button> </div>标签,那么传入的事件就会传给div标签

让div不继承属性

inheritAttrs: false,让div不继承属性。

<template>s
  <div>
    <button>
      <slot/>
    </button>
  </div>
</template>
<script lang="ts">
export default {
  inheritAttrs: false  //继承属性改为false,就是取消绑定
}
</script>

如果想让div里面的button绑定属性,可以用v-bind=$attrs,就是这个对象的所有属性都要绑定到button上面

<template>
  <div>
    <button v-bind="$attrs">
      <slot />
    </button>
  </div>
</template>
<script lang="ts">
export default {
  inheritAttrs: false  
}
</script>

想让一部分事件出现在div上,一部分出现在button上

<template>
  <div :size="size"> //div上绑定size事件
    <button v-bind="rest"> //button上绑定除size以外的事件
      <slot />
    </button>
  </div>
</template>
<script lang="ts">
export default {
  inheritAttrs: false ,
  setup(props, context){
    const {size, ...rest} = context.attrs
    return {size, rest}
  }
}
</script>

...rest是ES6语法(剩余操作符)的小技巧

总结

  • 默认所有属性都绑定到根元素

  • 使用inheritattrs: false 可以取消默认绑定

  • 使用$attrs或者 context.attrs 获取所有属性

  • 使用v-bind="$attrs" 批量绑定属性

  • 使用const {size, level, ...xxx} = context.attrs将属性分开

props与attrs的区别

  1. props需要先声明才能获取值,而attrs则不用

  2. props声明过的属性,attrs里面不会再出现

  3. props不包含事件,attrs包含

  4. props支持string以外的类型,而attrs只有string类型

让button支持theme属性

theme 的值为 button / link / text

Button.vue

<template>
  <button class="circle-button" :class="{ [`theme-${theme}`]: theme }">
    <slot />
  </button>
</template>
<script lang="ts">
export default {
  props: {
    theme: {
      type: String,
      default: "button",
    },
  },
};

ButtonDemo.vue

<template>
  <div>Button 示例</div>
  <h1>示例1</h1>
  <div>
    <Button>你好</Button>
    <Button theme="button">你好</Button>
    <Button theme="link">你好</Button>
    <Button theme="text">你好</Button>
  </div>
</template>
<script lang="ts">
import Button from "../lib/Button.vue";
export default {
  components: { Button },
  setup() {
    const onClick = () => {
      console.log("hi");
    };
    return { onClick };
  },
};
</script>

UI库的CSS注意事项

不能使用scoped

因为data-v-xxx中的xxx每次运行可能不同

必须输出稳定不变的class选择器,方便使用者覆盖

每个CSS类必须加前缀

.button 不行,很容易被使用者覆盖

.ciecle-button 可以,不太容易被覆盖

.theme-link 不行,很容易被使用者覆盖

.circle-theme-link 可以,不太容易被覆盖

注意:CSS最小影响原则(CSS绝对不能影响库使用者

button支持的属性

button 支持 size 属性

size 的值为 big / normal / small

button 支持 level 属性

main / normal / danger

button 支持 disabled 属性

disabled的值为 true/false

<button disabled>

<button :disabled="true">

<button disabled="true">

<button disabled= "false">

让 button 支持 loading 属性

loading 值为 true / false

代码

Button.vue

<template>
  <button class="circle-button" :class="classes" :disabled="disabled">
    <span v-if="loading" class="circle-loadingIndicator"></span>
    <slot />
  </button>
</template>
<script lang="ts">
import { computed } from "vue";
export default {
  props: {
    theme: {
      type: String,
      default: "button",
    },
    size: {
      type: String,
      default: "normal",
    },
    level: {
      type: String,
      default: "normal",
    },
    disabled: {
      type: Boolean,
      default: false,
    },
    loading: {
      type: Boolean,
      default: false,
    },
  },
  setup(props) {
    const { theme, size, level } = props;
    const classes = computed(() => {
      return {
        [`circle-theme-${theme}`]: theme,
        [`circle-size-${size}`]: size,
        [`circle-level-${level}`]: level,
      };
    });
    return { classes };
  },
};
</script>
<style lang="scss">
$h: 32px;
$border-color: #d9d9d9;
$color: #333;
$blue: #40a9ff;
$radius: 4px;
$red: red;
$grey: grey;
.circle-button {
  box-sizing: border-box;
  height: $h;
  padding: 0 12px;
  cursor: pointer;
  display: inline-flex;
  justify-content: center;
  align-content: center;
  white-space: nowrap;
  background: white;
  color: $color;
  border: 1px solid $border-color;
  border-radius: $radius;
  box-shadow: 0 1px 0 fade-out(black, 0.95);
  transition: background 250ms;
  & + & {
    margin-left: 8px;
  }
  &:hover,
  &:focus {
    color: $blue;
    border-color: $blue;
  }
  &:focus {
    outline: none;
  }
  &::-moz-focus-inner {
    border: 0;
  }
  &.circle-theme-link {
    border-color: transparent;
    box-shadow: none;
    color: $blue;
    &.hover,
    &:focus {
      color: lighten($blue, 10%);
    }
  }
  &.circle-theme-text {
    border-color: transparent;
    box-shadow: none;
    color: inherit;
    &:hover,
    &:focus {
      background: darken(white, 5%);
    }
  }
  &.circle-size-big {
    font-size: 24px;
    height: 48px;
    padding: 0 16px;
  }
  &.circle-size-small {
    font-size: 12px;
    height: 20px;
    padding: 0 4px;
  }
  &.circle-theme-button {
    &.circle-level-main {
      background: $blue;
      color: white;
      border-color: $blue;
      &:hover,
      &:focus {
        background: darken(white, 5%);
        border-color: darken($blue, 10%);
      }
    }
    &.circle-level-danger {
      background: $red;
      border-color: $red;
      color: white;
      &:hover,
      &:focus {
        background: darken($red, 10%);
        border-color: darken($red, 10%);
      }
    }
  }
  &.circle-theme-link {
    &.circle-level-danger {
      color: $red;
      &.hover,
      &.focus {
        color: darken($red, 10%);
      }
    }
  }
  &.circle-theme-text {
    &.circle-level-main {
      color: $blue;
      &:hover,
      &:focus {
        color: darken($blue, 10%);
      }
    }
    &.circle-level-danger {
      color: $red;
      &.hover,
      &.focus {
        color: darken($red, 10%);
      }
    }
  }
  &.circle-theme-button {
    &[disabled] {
      cursor: not-allowed;
      color: $grey;
      &:hover {
        border-color: $grey;
      }
    }
  }
  &.circle-theme-link,
  &.circle-theme-text {
    &[disabled] {
      cursor: not-allowed;
      color: $grey;
    }
  }
  > .circle-loadingIndicator {
    width: 14px;
    height: 14px;
    display: inline-block;
    margin-right: 4px;
    border-radius: 8px;
    border-color: $blue $blue $blue transparent;
    border-style: solid;
    border-width: 2px;
    animation: circle-spin 1s infinite linear;
  }
}
@keyframes circle-spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
</style>

ButtonDemo.vue

<template>
  <div>Button 示例</div>
  <h1>示例1</h1>
  <div>
    <Button>你好</Button>
    <Button theme="button">你好</Button>
    <Button theme="link">你好</Button>
    <Button theme="text">你好</Button>
  </div>
  <h1>示例2</h1>
  <div>
    <div>
      <Button size="big">大大大</Button>
      <Button>普通</Button>
      <Button size="small">小小小</Button>
    </div>
    <div>
      <Button theme="link" size="big">大大大</Button>
      <Button theme="link">普通</Button>
      <Button theme="link" size="small">小小小</Button>
    </div>
    <div>
      <Button theme="text" size="big">大大大</Button>
      <Button theme="text">普通</Button>
      <Button theme="text" size="small">小小小</Button>
    </div>
  </div>
  <h1>示例3</h1>
  <div>
    <div>
      <Button level="main">主要按钮</Button>
      <Button>普通按钮</Button>
      <Button level="danger">危险按钮</Button>
    </div>
    <div>
      <Button theme="link" level="main">主要链接按钮</Button>
      <Button theme="link">普通链接按钮</Button>
      <Button theme="link" level="danger">危险链接按钮</Button>
    </div>
    <div>
      <Button them e="text" level="main">主要文字按钮</Button>
      <Button theme="text">普通文字按钮</Button>
      <Button theme="text" level="danger">危险文字按钮</Button>
    </div>
  </div>
  <h1>示例4</h1>
  <div>
    <Button disabled>禁用按钮</Button>
    <Button theme="link" disabled>禁用按钮</Button>
    <Button theme="text" disabled>禁用按钮</Button>
  </div>
  <h1>示例5</h1>
  <div>
    <Button loading>加载中</Button>
  </div>
</template>
<script lang="ts">
import Button from "../lib/Button.vue";
export default {
  components: { Button },
  setup() {
    const onClick = () => {
      console.log("hi");
    };
    return { onClick };
  },
};
</script>