十、组件(3)

本章概要

  • 监听子组件事件
  • 在组件上使用 v-model 指令
    • v-model 的参数
    • 处理 v-model 的修饰符

10.3 监听子组件事件

在 Vue.js 中,子组件的某些功能与父组件的通信,是通过自定义事件实现的。
子组件使用 emit() 方法触发事件,父组件使用 v-on 指令监听子组件的自定义事件。
emit() 方法的语法形式如下:

$emit(eventName,[]...args)

eventName 为事件名,args为附件参数,这些参数会传给事件监听器的回调函数。如果子组件需要向父组件传递数据,就可以通过第二个参数来传。
如下:

app.component('child', {
  data: function () {
    return {
      name: '张三'
    }
  },
  methods: {
    handleClick() {
      this.$emit('greet', this.name);
    }
  },
  template: '<button @click="handleClick">开始欢迎</button>'
});

子组件的按钮接收到 click 时间后,调用 emit() 方法触发一个自定义事件。使用组件时,可以使用 v-on 指令监听 greet 事件。代码如下:

<div id="app">
  <child @greet="sayHello"></child>
</div>
<script>
  const app = Vue.createApp({
    methods: {
      // 自定义事件的附加参数会自动传入方法
      sayHello(name) {
        alert("Hello, " + name);
      }
    }
  });
  app.mount('#app');
</script>

与组件和 prop 不同,事件名不提供任何自动大小写转换。调用 emit() 方法触发的事件名称与用于监听该事件名称要完全匹配。
如果在 v-on 指令中直接使用 JavaScript 语句,则可以通过 event 访问自定义事件的附加参数。例如,在子组件中:

  <button @click="$emit('enlarge-text',0.1)">
    Enlarge text
  </button>

在父组件的模版中:

<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>

下面是个实际的例子。
通过帖子列表功能设计两个组件,实现一个 BBS 项目:PostList 和 PostListItem ,PostList 负责整个帖子列表的渲染,PostListItem 负责单个帖子的渲染。
帖子列表数据在 PostList 组件中维护,当增加新帖子或删除旧帖子时,帖子列表数据会发生变化,从而引起整个列表数据的重新渲染。
这里有一个问题,就是每个帖子都有一个“点赞”按钮,当点击按钮时,点击数加1,对于单个帖子,除了点赞数要变化外,其它信息(如标题、发帖人等)都不会变化,那么如果在 PostListItem 中维护点赞数,状态的管理就会比较混乱,子组件和父组件都会有状态变化,显然这不是很合理。
为此,在 PostList 中维护点赞数,而把 PostListItem 设计成无状态组件,这样所有的状态都在父组件中维护。
“点赞”按钮在子组件中,为了向父组件通知单击事件,可以使用自定义事件的方式,通过 emit() 方法触发,父组件通过 v-on 指令监听自定义事件。如下:

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
</head>

<body>
	<div id="app">
		<post-list></post-list>
	</div>

	<script src="https://unpkg.com/vue@next"></script>
	<script>
		const app = Vue.createApp({});
		// 子组件
		const PostListItem = {
			methods: {
				handleVote() {
					// 触发自定义事件
					this.$emit('vote');
				}
			},
			props: ['post'],
			template: `
        	        <li>
        	      		<p>
        	      			<span>标题:{{post.title}} | 发帖人:{{post.author}} | 发帖时间:{{post.date}} | 点赞数:{{post.vote}}</span>
        	      			<button @click="handleVote">赞</button>
        	      		</p>
        	      	</li>`
		};
		// 父组件
		app.component('PostList', {
			data() {
				return {
					posts: [
						{ id: 1, title: '2022年的第一场雪', author: '张三', date: '2019-10-21 20:10:15', vote: 0 },
						{ id: 2, title: '2021年的第一场雪', author: '李四', date: '2019-10-10 09:15:11', vote: 0 },
						{ id: 3, title: '2020年的第一场雪', author: '王五', date: '2020-11-11 15:22:03', vote: 0 }
					]
				}
			},
			components: {
				PostListItem
			},
			methods: {
				// 自定义事件vote的事件处理器方法
				handleVote(id) {
					this.posts.map(item => {
						item.id === id ? { ...item, vote: ++item.vote } : item;
					})
				}
			},
			template: `
        	      	<div>
        	      		<ul>
        	      			<PostListItem 
        	      				v-for="post in posts" 
        	      				:key="post.id" 
        	      				:post="post" 
        	      				@vote="handleVote(post.id)"/> <!--监听自定义事件-->
        	      		</ul>
        	      	</div>`
		});

		app.mount('#app');
	</script>
</body>

</html>

渲染结果如下:
在这里插入图片描述

在子组件中触发的事件可以在 emits 选项中进行定义。例如:

app.component('custom-form',{
  emits:['in-focus','submit']
})

如果在 emits 选项中定义了原生事件(如click事件),那么将使用组件事件而不是原生事件监听器。
Vue 3.0 删除了 v-on 指令的 .native 修饰符,该修饰符用于让组件可以监听原生事件,而现在, Vue 3.0 会把子组件中未定义为组件触发的时间的所有事件监听器作为原生事件监听器,添加到子组件的根元素上(除非在子组件的选项中设置了 inheritAttrs:false)。
建议在组件中定义所有要触发的事件,以便可以更好地记录组件应该如何工作。
与 prop 的类型验证类似,也可以采用对象语法而不是数组语法为定义的事件进行验证。要添加验证,需要为事件分配一个函数,该函数接收传递给 emit 调用的参数,并返回一个布尔值以指示事件是否有效。
代码如下:

const app = Vue.createApp({});
app.component('custom-form',{
  emits:{
    // 无验证
    click:null,
    // 验证 submit 事件
    submit:({email,password}) =>{
      if(email && password){
        return true;
      }else{
        console.warn('Invalid submit event payload !')
        return false;
      }
    }
  },
  methods:{
    submitForm(){
      this.$emit('submit',{email,password})
    }
  }
})

10.4 在组件上使用 v-model 指令

在表单元素上使用 v-model 指令可以实现数据双向绑定。如下:

<input type="text" v-model="message" />

等同于

<input :value="message" @input="message = $event.target.value" />

很多表单 UI 组件都是对 HTML 的表单控件的封装,在使用这些 UI 组件时,也可以使用 v-model 指令实现数据双向绑定。但是在组件上使用 v-model 指令时,情况会有所不同,如下:

<my-input v-model="message"></my-input>

v-model 会执行下面的操作:

<my-input :model-value="message" @update:model-value="message = $event" ></my-input>

这样的话,组件内部 input 元素就必须将 value 属性绑定到 modelValue prop 上,在 input 事件发生时,使用新的输入值触发 update:modelValue 事件。按照这个要求,可以给出如下的 MyInput 组件的实现代码:

const app = Vue.createApp({
    data() {
        return {
            message: 'ga beng cui'
        }
    }
});

app.component('MyInput', {
    props: ['modelValue'],
    template:`
        <input :value="modelValue" @input="$emit('update:modelValue',$event.target.value)"
    `,
});

在自定义组件中创建 v-model 功能的另一种方法是使用计算属性,在计算属性中定义 get() 和 set() 方法,get() 方法返回 modelValue 属性或用于绑定的任何属性, set() 方法为该属性触发相应的 emit。
修改上述 MyInput 组件的代码如下所示:

app.component('MyInput', {
    props: ['modelValue'],
    template: `
        <input v-model="value">
    `,
    computed: {
        value: {
            get(){
                return this.modelValue
            },
            set(newValue){
                this.$emit('update:modelValue', newValue);
            }
        }
    }
});

完成代码如下:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
</head>

<body>
    <div id="app">
        <my-input v-model="message"></my-input>
    </div>

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        const app = Vue.createApp({
            data() {
                return {
                    message: 'ga beng cui'
                }
            }
        });

        app.component('MyInput', {
            data: function () {
                return {
                    inpuStyles: {
                        'background-color': '#cdcdcd',
                        opacity: 0.5
                    },
                }
            },
            props: ['modelValue'],
            template: `
                    <div>
                        <input :value="modelValue"
                            :style="inpuStyles"
                            @input="$emit('update:modelValue', $event.target.value)">
                        <label>{{ modelValue }}</label>
                    </div>
                `
        });
        const vm = app.mount('#app');
    </script>
</body>

</html>

渲染效果如下:
在这里插入图片描述

在 Console 窗口中输入 vm.message=“hello”,可以看到文本输入空间中的内容和 label 元素的内容都发生了改变,如下:
在这里插入图片描述

10.4.1 v-model 的参数

默认情况下,组件上的 v-model 使用 modelValue 作为 prop ,update:modelValue 作为事件,可以给v-model 指令传递一个 参数来修改默认的名称。
例如,要使用名字title 作为 prop ,可以将 title 作为参数传递给 v-model 指令。如下:

<my-input v-model:title="message"></my-input>

在这种情况下,MyInput 组件需要一个 title prop ,以及触发 update:title 事件保证同步。如下:

app.component('MyInput', {
  props: {
      title: String  
  },
    template: `
        <div>
            <input :value="title"
                :style="inpuStyles"
                @input="$emit('update:title', $event.target.value)">
            <label>{{ title }}</label>
        </div>
    `	
});

从 Vue 3.0 开始,可以在同一个组件上进行多个 v-model 绑定。利用 v-model 的参数机制,每个 v-model 可以同步到不同的 prop ,而不需要在组件中添加额外的选项。
如下:

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
</head>

<body>
	<div id="app">
		<user-name v-model:first-name="fistName" v-model:last-name="lastName"></user-name>
	</div>

	<script src="https://unpkg.com/vue@next"></script>
	<script>
		const app = Vue.createApp({});
		app.component('user-name',{
            props: {
                firstName:String,
                lastName:String
            },
            template:`
                <input type="text" :value="firstName" @input="$emit('update:firstName',$event.target.value)" />
                <input type="text" :value="lastName" @input="$emit('update:lastName',$event.target.value)" />
            `
		})
		app.mount('#app');
	</script>
</body>

</html>

10.4.2 处理 v-model 的修饰符

创建一个自定义修饰符 capitalize ,它将 v-model 绑定提供的字符串的第一个字母大写。添加到组件 v-model 的修饰符将通过 modelModifiers prop 提供给组件。如下:

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
</head>

<body>
	<div id="app">
		<my-input v-model.capitalize="message"></my-input>
	</div>

	<script src="https://unpkg.com/vue@next"></script>
	<script>
		const app = Vue.createApp({});
		app.component('MyInput',{
            props: {
                modelValue:String,
                // modelValue prop 默认为空对象
                modelModifiers:{
                    default:() => ({})
                }
            },
            template:`
                <div>
                    <input :value="modelValue" @input="$emit('update:modelValue',$event.target.value)" />
                    <lable>{{ modelValue }}</lable>
                </div>`,
                created() {
                    console.log(this.modelModifiers) // {capitalize:true}
                },
		})
		app.mount('#app');
	</script>
</body>

</html>

当组件的 created 声明周期钩子触发时,modelModifiers prop 包含 capitalize 属性,它的属性为 true。接下来可以通过检查 capitalize 值的真假,在 input 元素触发 input 事件时,将字符串的首字母大写。如下:

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
</head>

<body>
	<div id="app">
		<my-input v-model.capitalize="message"></my-input>
	</div>

	<script src="https://unpkg.com/vue@next"></script>
	<script>
		const app = Vue.createApp({
            data(){
                return {
                    message:''
                }
            }
        });
		app.component('MyInput',{
            props: {
                modelValue:String,
                // modelValue prop 默认为空对象
                modelModifiers:{
                    default:() => ({})
                }
            },
            methods: {
                emitValue(e){
                    let value = e.target.value;
                    if (this.modelModifiers.capitalize){
                        value = value.charAt(0).toUpperCase() + value.slice(1)
                    }
                    this.$emit('update:modelValue',value)
                }
            },
            template:`
                <div>
                    <input :value="modelValue" @input="emitValue" />
                    <lable>{{ modelValue }}</lable>
                </div>`,
                created() {
                    console.log(this.modelModifiers) // {capitalize:true}
                },
		})
		app.mount('#app');
	</script>
</body>

</html>

在文本输入框中输入英文字符,其首字母会自动转换为大写。
对于带参数的 v-model 绑定,生成的 prop 的名字是 arg + “Modifiers”。如下:

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
</head>

<body>
	<div id="app">
		<my-input v-model:title.capitalize="message"></my-input>
	</div>

	<script src="https://unpkg.com/vue@next"></script>
	<script>
		const app = Vue.createApp({
            data() {
                return {
                    message:''
                }
            },
        });
		app.component('MyInput',{
            props: {
                title:String,
                titleModifiers:{
                    default:() => ({})
                }
            },
            methods: {
                emitValue(e){
                    let value = e.target.value;
                    if (this.titleModifiers.capitalize){
                        value = value.charAt(0).toUpperCase() + value.slice(1)
                    }
                    this.$emit('update:title',value)
                }
            },
            template:`
                <div>
                    <input :value="title" @input="emitValue" />
                    <lable>{{ title }}</lable>
                </div>`
		})
		app.mount('#app');
	</script>
</body>

</html>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只小熊猫呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值