4

效果

本节要实现的是导航标签切换功能:

clipboard.png

html
<div class="tabs">
  <ul>
    <li class="is-active"><a>Pictures</a></li>
    <li><a>Music</a></li>
    <li><a>Videos</a></li>
    <li><a>Documents</a></li>
  </ul>
</div>

实现

首先来考虑标签如何实现,我们使用 name 属性让用户定义标签:

<tab name="图片"></tab>
<tab name="音乐"></tab>
<tab name="文档"></tab>
<tab name="视频"></tab>

定义具体的 tab 组件:

Vue.component('tab',{
    template:`
        <div></div>
    `,
    mounted(){
        console.log(this);
    }
});

我们打印出组件的对象,发现 name 的值并没有传递进来:

clipboard.png

其实就是之前讲过的,组件的实例要传递数据给组件,必须在 props 中声明:

Vue.component('tab',{
    props:['name'],
    template:`
        <div></div>
    `,
    mounted(){
        console.log(this);
    }
});

现在,组件对象里就可以看到 name 传递进来了。

clipboard.png

接下来是 zen-tabs

Vue.component('zen-tabs',{
    template:`
        <div><slot></slot></div>
    `
});

里面定义了一个 slot ,以便用于自定义 tab,比如:

<zen-tabs>
    <tab name="图片"></tab>
    <tab name="音乐"></tab>
    <tab name="文档"></tab>
    <tab name="视频"></tab>
</zen-tabs>

现在的问题是,zen-tabs 组件如何获取 name 数据呢?我们不妨打印出来看看:

Vue.component('zen-tabs',{
    template:`
        <div><slot></slot></div>
    `,
    mounted(){
        console.log(this);
    }
});

效果如下:

clipboard.png

也就是说,如果一个组件(zen-tabs,称之为父组件)里面使用了另外一个组件(tab,称之为子组件),那么可以通过 $children 获取子组件的数据。

Vue.component('zen-tabs',{
    template:`
        <div><slot></slot></div>
    `,
    mounted(){
        this.tabs = this.$children;
    },
    data(){
        return {
            tabs:[]
        }
    }
});

现在,我们将子组件的数据赋值给了 tabs 变量了,然后就可以使用了:

Vue.component('zen-tabs',{
    template:`
        <div>
            <div class="tabs">
                <ul>
                    <li v-for="tab in tabs"><a href="#">{{tab.name}}</a></li>
                </ul>
            </div>
            <div><slot></slot></div>
        </div>
        
    `,
    mounted(){
        this.tabs = this.$children;
    },
    data(){
        return {
            tabs:[]
        }
    }
});

效果如下:

clipboard.png

接下来标签的激活功能。首先,我们为第一个标签添加激活功能看看:

<div id="root" class="container">    
    <zen-tabs>
        <tab name="图片" selected="true"></tab>
        <tab name="音乐"></tab>
        <tab name="文档"></tab>
        <tab name="视频"></tab>
    </zen-tabs>
</div>

tab 组件中在 props 中定义 selected,并赋予默认值:

Vue.component('tab',{
    props: {
        name:{require:true},
        selected: {default:false}
    },
    template:`
        <div></div>
    `
});

最后,可以通过 selected 的值来决定是否添加激活类 is-active:

Vue.component('zen-tabs',{
    template:`
        <div>
            <div class="tabs">
                <ul>
                    <li v-for="tab in tabs" :class="{'is-active':tab.selected === true}">
                        <a href="#">{{tab.name}}</a>
                    </li>
                </ul>
            </div>
            <div><slot></slot></div>
        </div>
        
    `,
    mounted(){
        this.tabs = this.$children;
    },
    data(){
        return {
            tabs:[]
        }
    }
});

发现没效果:

clipboard.png

这是因为,我们使用的的是 selected = "true",这种写法只能传递字面量,因此,传递的是字符串 "true",而我们使用了 === 来判断传入的到底是不是布尔值 true,结果就返回 false 了。

因此,如果要动态的传递属性,需要使用:

<tab name="图片" :selected="true"></tab>

这样话 "true" 就被当成表达式来解析了,就为布尔值 true 了。修改之后,效果就出来了:

clipboard.png

接下来,就可以根据用户的点击来动态切换标签了:

component('zen-tabs',{
    template:`
        <div>
            <div class="tabs">
                <ul>
                    <li v-for="tab in tabs" :class="{'is-active':tab.selected === true}" @click="selectTab(tab)">
                        <a href="#">{{tab.name}}</a>
                    </li>
                </ul>
            </div>
            <div><slot></slot></div>
        </div>
        
    `,
    mounted(){
        this.tabs = this.$children;
    },
    data(){
        return {
            tabs:[]
        }
    },
    methods:{
        selectTab(selectedTab){
            this.tabs.forEach(function(tab){
                tab.selected= (selectedTab.name == tab.name);
            })
        }
    }
});

这样做,理论上是没问题的,实际上,会报错:

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "selected"

为什么 Vue 不提倡这样做,因为我们在组件里面修改 selected 的值,这样可能会对外部造成影响,为了保持松耦合,请将 props 仅仅当成是一种传递数据(而非改变数据)的方式。我们可以自己在内部定义变量:

Vue.component('tab',{
    props: {
        name:{require:true},
        selected: {default:false}
    },
    template:`
        <div></div>
    `,
    mounted(){
        this.isActive = this.selected;
    },
    data(){
        return {
            isActive:false
        }
    }
});
Vue.component('zen-tabs',{
    template:`
        <div>
            <div class="tabs">
                <ul>
                    <li v-for="tab in tabs" :class="{'is-active':tab.isActive=== true}" @click="selectTab(tab)">
                        <a href="#">{{tab.name}}</a>
                    </li>
                </ul>
            </div>
            <div><slot></slot></div>
        </div>
        
    `,
    mounted(){
        this.tabs = this.$children;
    },
    data(){
        return {
            tabs:[]
        }
    },
    methods:{
        selectTab(selectedTab){
            this.tabs.forEach(function(tab){
                tab.isActive= (selectedTab.name == tab.name);
            })
        }
    }
});

最后,优化一下该组件,首先是允许用户自定义视图:

<zen-tabs>
    <tab name="图片" :selected="true">图片视图</tab>
    <tab name="音乐">音乐视图</tab>
    <tab name="文档">文档视图</tab>
    <tab name="视频">视频视图</tab>
</zen-tabs>

只需要稍微修改下 tab 的模板:

Vue.component('tab',{
    template:`
        <div v-show="isActive">
            <slot></slot>
        </div>
    `

最后是超链接功能,用计算属性来实现:

Vue.component('tab',{
    computed:{
        href(){
            return '#' + this.name.toLowerCase().replace(/ /g,'-');
        }
    }
});

Vue.component('zen-tabs',{
    template:`
        <div>
            <div class="tabs">
                <ul>
                    <li v-for="tab in tabs" :class="{'is-active':tab.isActive=== true}" @click="selectTab(tab)">
                        <a :href="tab.href">{{tab.name}}</a>
                    </li>
                </ul>
            </div>
            <div><slot></slot></div>
        </div>    
    `,

通过计算属性,让超链接返回 # + 标签名 的方式,如果标签名中存在空格,就用 - 来代替。


附录:


心智极客
1k 声望645 粉丝