Vue2自定义指令和插槽

自定义指令

  • 为什么要自定义指令?当我们需要操作DOM,但每次使用$nextTick太繁琐,我们希望可以优雅封装为一个指令,只要在DOM元素中,添加这个指令,例如 v-foucus ,输入框DOM元素能自动获取焦点

全局注册指令

定义指令(main.js)

  • 全局指令,所有组件都能使用
import Vue from 'vue'
import App from './App.vue'

// 注意:不需要加v-,Vue会自动帮我们加上,使用的时候使用v-foucus
// Vue.directive('v-foucs');// 错误写法

// 全局注册,所有组件都可以使用,局部注册只能在注册的组件中使用
Vue.directive('foucs', {
  // 当指令所在的标签,被渲染到页面的时候,回调
  inserted(element) {
    // element 就是 DOM 元素,谁用这个指令,那个DOM元素就是element
    element.focus();
  }
});

Vue.config.productionTip = false
new Vue({
  render: h => h(App),
}).$mount('#app')

使用指令(App.vue)

<template>
  <div>
    <h1>v-focus 自动获得焦点</h1>
    <!-- v-focus,自定义指令,能让 input 输入框渲染到页面时,自动获取焦点 -->
    <input type="text" v-foucs />
  </div>
</template>

<script>
export default {
};
</script>

<style>
</style>

局部注册指令

  • 局部指令,只有注册了指令的组件才能使用
// App.vue
<template>
  <div>
    <h1>v-focus 自动获得焦点</h1>
    <!-- v-focus,自定义指令,能让 input 输入框渲染到页面时,自动获取焦点 -->
    <input type="text" v-foucs />
  </div>
</template>

<script>
export default {
  // 局部注册指令
  directives: {
    foucs: {
      // element是使用的该指令的DOM元素
      inserted(element) {
        // 操作DOM元素,获取焦点
        element.focus();
      },
    },
  },
};
</script>

<style>
</style>

获取指令的值

  • 自定义指令和Vue提供的指令一样,自定义指令同样可以设置值和获取值

  • 下面就来封装一个和v-html一样功能的v-hml指令,功能是将指令的值设置给DOM元素的HTML,实现设置innerHTML的功能

  • inserted(el, binding),当绑定指令的DOM元素,渲染到页面时回调

  • update(el, binding),当指令绑定的值,发生改变时回调

<template>
  <div>
    <!-- 点击按钮时,将下面div的内容,从h1改为button按钮 -->
    <button @click="change">修改</button>
    <div v-hml="msg"></div>
  </div>
</template>

<script>
export default {
  // 局部注册指令
  directives: {
    // 指令名称,注意不能叫html,会和Vue定义好的发生冲突
    hml: {
      // 绑定指令的DOM元素,渲染到页面时回调
      inserted(el, binding) {
        // 更新DOM元素的innerHTML
        el.innerHTML = binding.value;
      },
      // 指令绑定的值,发生改变时回调
      update(el, binding) {
        el.innerHTML = binding.value;
      },
    },
  },
  data() {
    return {
      msg: "<h1>我是大标题</h1>",
    };
  },
  methods: {
    change() {
      // 将div的内容,从h1改为button按钮
      this.msg = "<button>我变成按钮啦</button>";
    },
  },
};
</script>

<style>
</style>

插槽

  • 当一个组件的样式有很多种,但大体框架不变时,使用插槽,可以将会变的部分,让父组件提供,子组件只提供基础框架和样式,提高组件灵活性和可拓展性
  • 通过slot标签,也就是插槽标签,现在子组件中进行占位,父组件则提供不同的可变部分组件,Vue会将这部分组件填充到占位处

默认插槽

  • 默认插槽,也就是没有名字插槽,也就是子组件只有一部分可提供给父组件设置
  • 其实默认插槽也是一种具名插槽,只是名字叫default
  • 下面,自定义一个弹窗组件,其中内容部分可变,可为文字、按钮、其他标签等

弹窗子组件(MyDialog.vue)

<template>
  <div class="dialog">
    <div class="dialog-header">
      <h3>友情提示</h3>
      <span class="close">✖️</span>
    </div>

    <div class="dialog-content">
      <!-- 内容,使用插槽,使用组件时,添加标签的内容,内容就会替换到插槽的位置 -->
      <slot></slot>
    </div>
    <div class="dialog-footer">
      <button>取消</button>
      <button>确认</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {};
  },
};
</script>

<style scoped>
* {
  margin: 0;
  padding: 0;
}
.dialog {
  width: 470px;
  height: 230px;
  padding: 0 25px;
  background-color: #ffffff;
  margin: 40px auto;
  border-radius: 5px;
}
.dialog-header {
  height: 70px;
  line-height: 70px;
  font-size: 20px;
  border-bottom: 1px solid #ccc;
  position: relative;
}
.dialog-header .close {
  position: absolute;
  right: 0px;
  top: 0px;
  cursor: pointer;
}
.dialog-content {
  height: 80px;
  font-size: 18px;
  padding: 15px 0;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
}
.dialog-footer button {
  width: 65px;
  height: 35px;
  background-color: #ffffff;
  border: 1px solid #e1e3e9;
  cursor: pointer;
  outline: none;
  margin-left: 10px;
  border-radius: 3px;
}
.dialog-footer button:last-child {
  background-color: #007acc;
  color: #fff;
}
</style>

父组件(App.vue)

<template>
  <div>
    <!-- 默认插槽,只要给子组件的标签内容,添加内容,就会被替换到组件内部的 slot 标签中 -->
    <MyDialog>123</MyDialog>

    <!-- 填充按钮到插槽中 -->
    <MyDialog>
      <button>关闭</button>
    </MyDialog>

    <!-- 填充一个标题到插槽中 -->
    <MyDialog>
      <h1>大大的标题</h1>
    </MyDialog>
  </div>
</template>

<script>
import MyDialog from "./components/MyDialog.vue";

export default {
  data() {
    return {};
  },
  // 注册子组件
  components: {
    MyDialog,
  },
};
</script>

<style>
body {
  background-color: #b3b3b3;
}
</style>

后备内容

  • 后备内容,也就是插槽的默认内容,当父组件没有填充组件或内容时,则使用子组件设置的默认内容

子组件(MyDialog.vue)

<template>
  <div class="dialog">
    <div class="dialog-header">
      <h3>友情提示</h3>
      <span class="close">✖️</span>
    </div>

    <div class="dialog-content">
      <!-- 往slot标签内部,编写内容,可以作为后备内容(默认值) -->
      <slot>
        <h5>我是默认内容</h5>
      </slot>
    </div>
    <div class="dialog-footer">
      <button>取消</button>
      <button>确认</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {};
  },
};
</script>

<style scoped>
// ...
</style>

父组件(App.vue)

<template>
  <div>
    <!-- 传内容,则使用我们传的内容 -->
    <MyDialog>
      <button>点我</button>
    </MyDialog>
    <!-- 不传内容,会使用默认值 -->
    <MyDialog></MyDialog>
  </div>
</template>

<script>
import MyDialog from "./components/MyDialog.vue";
export default {
  data() {
    return {};
  },
  components: {
    MyDialog,
  },
};
</script>

<style>
body {
  background-color: #b3b3b3;
}
</style>

具名插槽

  • 具名插槽,当子组件有好几个部分都需要暴露出来,给父组件设置时使用,其实就是插槽有名字,父组件填充内容时,必须指定填充的是哪个名字的插槽

  • 下面,弹窗子组件需要将头部、内容、尾部,暴露这3部分给父组件

  • 通过name属性,给插槽命名

子组件(MyDialog.vue)

<template>
  <div class="dialog">
    <div class="dialog-header">
      <!-- 头部 -->
      <slot name="head"></slot>
    </div>
    <div class="dialog-content">
      <!-- 内容 -->
      <slot name="content"></slot>
    </div>
    <div class="dialog-footer">
      <!-- 尾部 -->
      <slot name="footer"></slot>
    </div>

    <!-- 其实默认插槽,也是具名插槽,只是名称叫default -->
    <!-- <slot name="default"></slot> -->
  </div>
</template>

<script>
export default {
  data() {
    return {};
  },
};
</script>

<style scoped>
// ...
</style>

父组件(App.vue)

  • 父组件,通过v-slot:插槽名称来指定插入的内容,对应子组件的哪个插槽
  • 号,是 v-slot: 的简写,开发中常用简写

<template>
  <!-- 
    目前学到的指令简写:
    @ => v-on,事件监听
    # => v-slot:,插槽
    : => v-bind,动态绑定属性
   -->
  <div>
    <MyDialog>
      <!-- 通过template标签,将要添加插槽内容标签 -->
      <!-- 通过v-slot属性,指定插槽的名称 -->

      <!-- 头部 -->
      <template v-slot:head>
        <h1>大标题</h1>
      </template>

      <!-- 内容 -->
      <template v-slot:content>
        <h4>我是内容</h4>
      </template>

      <!-- 尾部 -->
      <!-- #号,是 v-slot: 的简写 -->
      <template #footer>
        <button>确定</button>
      </template>

      <!-- 默认插槽,其实也是具名插槽,只是名称叫default,所以也可以使用具名插槽的写法,插槽名字叫default -->
      <!-- <template v-slot:default>默认插槽的内容</template> -->
    </MyDialog>
  </div>
</template>

<script>
import MyDialog from "./components/MyDialog.vue";

export default {
  data() {
    return {};
  },
  components: {
    MyDialog,
  },
};
</script>

<style>
body {
  background-color: #b3b3b3;
}
</style>

作用域插槽

  • 使用具名插槽,已经能实现多个部分暴露给父组件进行设置,但有一个问题还没有解决,就是子组件的数据,如何通过插槽抛出给父组件,解决方案就是作用域插槽。
  • 作用域插槽,是在具名插槽的基础上实现的,通过v-bind,将子组件的属性绑定给插槽,父组件在提供内容时,就能获取这些属性
  • 下面,让一个表格子组件,提供一个按钮位置的插槽,并将当前行的数据id回传给父组件,父组件就能使用这个id,实现查看按钮、删除按钮

子组件(MyTable.vue)

  • 子组件,通过:id="item.id",来将这一行的数据id,提供给父组件使用
<template>
  <table class="my-table">
    <thead>
      <tr>
        <th>序号</th>
        <th>姓名</th>
        <th>年纪</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody>
      <!-- 遍历表格单元格 -->
      <tr v-for="(item, index) in list" :key="item.id">
        <td>{{ index + 1 }}</td>
        <td>{{ item.name }}</td>
        <td>{{ item.age }}</td>
        <td>
          <!-- 作用域插槽,将当前遍历到的数据的id,传出去 -->
          <!-- Vue会将id,包装成一个对象,父组件要通过 template 标签,声明接收这个对象,然后通过 对象.属性名 来获取这个id值 -->
          <slot :id="item.id"></slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

<script>
export default {
  props: {
    list: Array,
  },
};
</script>

<style scoped>
.my-table {
  width: 450px;
  text-align: center;
  border: 1px solid #ccc;
  font-size: 24px;
  margin: 30px auto;
}
.my-table thead {
  background-color: #1f74ff;
  color: #fff;
}
.my-table thead th {
  font-weight: normal;
}
.my-table thead tr {
  line-height: 40px;
}
.my-table th,
.my-table td {
  border-bottom: 1px solid #ccc;
  border-right: 1px solid #ccc;
}
.my-table td:last-child {
  border-right: none;
}
.my-table tr:last-child td {
  border-bottom: none;
}
.my-table button {
  width: 65px;
  height: 35px;
  font-size: 18px;
  border: 1px solid #ccc;
  outline: none;
  border-radius: 3px;
  cursor: pointer;
  background-color: #ffffff;
  margin-left: 5px;
}
</style>

父组件(App.vue)

  • 父组件,通过#插槽名="对象",来接受子组件提供的所有数据
  • 子组件slot插槽标签中,绑定的多个数据,都会给Vue包装为一对象,所以父组件使用id属性,需要通过对象.属性名来获取
<template>
  <div>
    <MyTable :list="list">
      <!-- 3. 通过template #插槽名="变量名" 接收数据 -->
      <template #default="obj">
        <!-- 通过obj.id,来获取子组件传递过来的id数据 -->
        <button @click="del(obj.id)">删除</button>
      </template>
    </MyTable>

    <MyTable :list="list">
      <!-- 3. 通过template #插槽名="变量名" 接收数据 -->
      <template #default="obj">
        <button @click="showDetail(obj.id)">查看</button>
      </template>
    </MyTable>
  </div>
</template>

<script>
import MyTable from "./components/MyTable.vue";
export default {
  data() {
    return {
      list: [
        { id: 1, name: "张小花", age: 18 },
        { id: 2, name: "孙大明", age: 19 },
        { id: 3, name: "刘德忠", age: 17 },
      ],
    };
  },
  methods: {
    // 删除
    del(id) {
      console.log(id);
      // 删除指定id的数据
      const index = this.list.findIndex((item) => item.id === id);
      this.list.splice(index, 1);
    },
    // 查看详情
    showDetail(id) {
      const item = this.list.find((item) => item.id === id);
      alert(JSON.stringify(item));
    },
  },
  components: {
    MyTable,
  },
};
</script>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容