原生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></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}">
<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>
</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:''
}
}
}
</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;
}
}
/*增加选中的样式*/
.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;
}
}
</style>