vue自定义radio组件

原生html的radio标签太丑了,实际开发中都需要自定义radio。如何实现呢?
思路很简单,我们需要另外做一套好看的样式,然后把原来的radio标签隐藏掉即可。

静态结构

components/myradio.vue:

<template>
    <div class="my-radio">
        <span class="my-radio__input">
            <span class="my-radio__inner"></span>
            <!--真实的radio,被隐藏,去掉.my-radio__original可以看到-->
            <input class="my-radio__original" type="radio" />
        </span>
        <span class="my-radio__label">
          说明文字
        </span>
    </div>
</template>

<script>
    export default {
        name: "MyRadio"
    }
</script>

<style lang="scss">
    .my-radio {
        color: #606266;
        font-weight: 500;
        line-height: 1;
        position: relative;
        cursor: pointer;
        display: inline-block;
        white-space: nowrap;
        outline: none;
        font-size: 14px;
        margin-right: 30px;
        -moz-user-select: none;
        -webkit-user-select: none;
        -ms-user-select: none;

        .my-radio__input {
            white-space: nowrap;
            cursor: pointer;
            outline: none;
            display: inline-block;
            line-height: 1;
            position: relative;
            vertical-align: middle;

            .my-radio__inner {
                border: 1px solid #dcdfe6;
                border-radius: 100%;
                width: 14px;
                height: 14px;
                background-color: #fff;
                position: relative;
                cursor: pointer;
                display: inline-block;
                box-sizing: border-box;
            }

            .my-radio__original {
                opacity: 0;
                outline: none;
                position: absolute;
                z-index: -1;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                margin: 0;
            }
        }

        .my-radio__label {
            font-size: 14px;
            padding-left: 10px;
        }
    }
</style>

main.js:

import Vue from 'vue'
import App from './App.vue'
import MyRadio from './components/myradio'
Vue.config.productionTip = false

Vue.component(MyRadio.name,MyRadio)
new Vue({
  render: h => h(App),
}).$mount('#app')

App.vue:

<template>
  <div id="app">
    <my-radio v-model="sex" label="01" disabled @click="sexClick"></my-radio>
  </div>
</template>

功能实现

<template>
  <div id="app">
    <my-radio label="0" v-model="sex">男</my-radio>
    <my-radio label="1" v-model="sex">女</my-radio>
  </div>
</template>
<script>
  export default {
    data(){
      return {
        sex: '0'
      }
    }
  }

</script>

需要实现的功能:

1、接收props

input一般都有name,所以一块加上。

<template>
    <div class="my-radio">

        <span class="my-radio__input">
            <span class="my-radio__inner"></span>
            <!--真实的radio,被隐藏,去掉.my-radio__original可以看到-->
            <input :name="name" :value="label" type="radio" class="my-radio__original" />
        </span>
        <span class="my-radio__label">
           说明文字
        </span>
    </div>
</template>

<script>
    export default {
        name: "MyRadio",
        props:{
            label:{ //接收label
                type:[String,Number,Boolean],
                default:''
            },
            value:null, // 接收v-model
            name:{ // 有可能传name
                type:String,
                default:''
            }


        }
    }
</script>

<style lang="scss">
    .my-radio {
        color: #606266;
        font-weight: 500;
        line-height: 1;
        position: relative;
        cursor: pointer;
        display: inline-block;
        white-space: nowrap;
        outline: none;
        font-size: 14px;
        margin-right: 30px;
        -moz-user-select: none;
        -webkit-user-select: none;
        -ms-user-select: none;

        .my-radio__input {
            white-space: nowrap;
            cursor: pointer;
            outline: none;
            display: inline-block;
            line-height: 1;
            position: relative;
            vertical-align: middle;

            .my-radio__inner {
                border: 1px solid #dcdfe6;
                border-radius: 100%;
                width: 14px;
                height: 14px;
                background-color: #fff;
                position: relative;
                cursor: pointer;
                display: inline-block;
                box-sizing: border-box;
            }

            .my-radio__original {
                opacity: 0;
                outline: none;
                position: absolute;
                z-index: -1;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                margin: 0;
            }
        }

        .my-radio__label {
            font-size: 14px;
            padding-left: 10px;
        }
    }
</style>

2、获取父组件的label文字

这里用slot插槽就行,不过考虑到这个label文字可能不传,直接用label的值,所以需要做个判断。

        <span class="my-radio__label">
           <slot></slot>
            <!--如果没传内容,就用label的值-->
            <template v-if="!$slots.default">{{label}}</template>
        </span>

3、v-model的处理

这里需要注意的是v-model,根据单向数据流动的原则,父组件传给子组件的value,子组件不能直接修改。但单选框肯定是要改value的,怎么办呢?
解决方案是:
子组件另外定义一个属性,不过不能在data里面直接定义,因为这个属性是跟着value走的,我们可以定义一个计算属性,并定义它的get和set,其中set里面用$emit触发input事件即可:

<template>
    <div class="my-radio">

        <span class="my-radio__input">
            <span class="my-radio__inner"></span>
            <!--真实的radio,被隐藏,去掉.my-radio__original可以看到-->
            <input v-model="model" :name="name" :value="label" class="my-radio__original" type="radio" />
        </span>
        <span class="my-radio__label">
           <slot></slot>
            <!--如果没传内容,就用label的值-->
            <template v-if="!$slots.default">{{label}}</template>
        </span>
    </div>
</template>

<script>
    export default {
        name: "MyRadio",
        computed:{
            model:{
                get(){
                    return this.value
                },
                set(val){
                    console.log(val)
                    this.$emit('input', val)
                }
            },
        },
        props:{
            label:{ //接收label
                type:[String,Number,Boolean],
                default:''
            },
            value:null, // 接收v-model
            name:{ // 有可能传name
                type:String,
                default:''
            }


        }
    }
</script>

<style lang="scss">
   ...
</style>

这里有个坑。这段代码测试是无效的,当点击radio时,set函数根本无法触发。为啥呢?
因为外层标签是div的话,实际上input已经被隐藏了,因此当你点击div的时候,并无法点到input上面去,导致v-model的双向绑定动作无法触发。怎么办呢?
我们可以将最外层标签换成label,因为label标签有个for属性,是指定需绑定哪个表单元素的。如果for为空,则会默认绑定到与label相关的表单元素上。
这里label标签包裹了input,因此就会默认绑定到input上面。

4、选中效果

    .is-checked {
        .my-radio__input{
            .my-radio__inner{
                border-color:#409eff;
                background: #409eff;
                &:after{
                    transform: translate(-50%,-50%) scale(1);
                }
            }
        }
        .my-radio__label{
            color:#409eff;
        }
    }

判断如果label和value的值是相等的,那就是选中状态。

<label class="my-radio" :class="{'is-checked': label === value}">
   ...
</label>

5、最终代码

<template>
  <label
    class="my-radio"
    :class="{'is-checked': label === value,'disabled':disabled}"
    @click="clickRadio"
  >
    <span class="my-radio__input">
      <span class="my-radio__inner"></span>
      <!--真实的radio,被隐藏,去掉.my-radio__original可以看到-->
      <input
        v-model="model"
        :name="name"
        :value="label"
        class="my-radio__original"
        type="radio"
        :disabled="disabled"
      />
    </span>
    <span class="my-radio__label">
      <slot></slot>
      <!--如果没传内容,就用label的值-->
      <template v-if="!$slots.default">{{label}}</template>
    </span>
  </label>
</template>

<script>
export default {
  name: "MyRadio",
  computed: {
    model: {
      get () {
        return this.value
      },
      set (val) {
        console.log(val)
        this.$emit('input', val)
      }
    },
  },
  props: {
    label: { //接收label
      type: [String, Number, Boolean],
      default: ''
    },
    value: null, // 接收v-model
    name: { // 有可能传name
      type: String,
      default: ''
    },
    disabled: {//是否禁用
      type: Boolean,
      default: false
    }
  },
  methods: {
    clickRadio (e) {
      // if (e.target.tagName === 'INPUT') return 
      // 获取当前点击的值
       //点击时会运行两遍、下面是当e.target.tagName时运行的。还有一个是label的过滤
      if (e.target.tagName === 'INPUT') {
        console.log(e.target.value)
        this.$emit('click', e.target.value)
      }

    }
  }
}
</script>
<style lang="scss" scoped>
.my-radio {
  color: #606266;
  font-weight: 500;
  line-height: 1;
  position: relative;
  cursor: pointer;
  display: inline-block;
  white-space: nowrap;
  outline: none;
  font-size: 14px;
  margin-right: 30px;
  -moz-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;

  .my-radio__input {
    white-space: nowrap;
    cursor: pointer;
    outline: none;
    display: inline-block;
    line-height: 1;
    position: relative;
    vertical-align: middle;

    .my-radio__inner {
      border: 1px solid #dcdfe6;
      border-radius: 100%;
      width: 14px;
      height: 14px;
      background-color: #fff;
      position: relative;
      cursor: pointer;
      display: inline-block;
      box-sizing: border-box;
    }
    .my-radio__inner:after {
      width: 4px;
      height: 4px;
      border-radius: 100%;
      background-color: #fff;
      content: '';
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%) scale(0);
      transition: transform 0.15s ease-in;
    }

    .my-radio__original {
      opacity: 0;
      outline: none;
      position: absolute;
      z-index: -1;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: 0;
    }
  }

  .my-radio__label {
    font-size: 14px;
    padding-left: 10px;
  }
}

/*增加选中的样式*/
.is-checked {
  .my-radio__input {
    .my-radio__inner {
      border-color: #409eff;
      background: #409eff;
      &:after {
        transform: translate(-50%, -50%) scale(1);
      }
    }
  }
  .my-radio__label {
    color: #409eff;
  }
}
.disabled {
  .my-radio__input {
    .my-radio__inner {
      &:after {
        transform: translate(-50%, -50%) scale(1);
      }
    }
  }
  .my-radio__label {
    color: #ccc;
  }
}
</style>
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值