Vue基础入门二
1.表单基本操作
获取单选框的值
-
通过V-model 同时必须要有value属性 ,value值不能一样
<!-- 1、 两个单选框需要同时通过v-model 双向绑定 一个值 2、 每一个单选框必须要有value属性 且value 值不能一样 3、 当某一个单选框选中的时候 v-model 会将当前的 value值 改变 data 中的 数据 gender 的值就是选中的值,我们只需要实时监控他的值就可以了 --> <input type="radio" id="male" value="1" v-model='gender'> <label for="male">男</label> <input type="radio" id="female" value="2" v-model='gender'> <label for="female">女</label> <script> new Vue({ data: { // 默认会让当前的 value 值为 2 的单选框选中 gender: 2, }, }) </script>
获取复选框
- 通过v-model
- 同时必须要有value属性,value值不能一样
- 复选框
checkbox
这种的组合时 data 中的 hobby 我们要定义成数组 否则无法实现多选
<!--
1、 复选框需要同时通过v-model 双向绑定 一个值
2、 每一个复选框必须要有value属性 且value 值不能一样
3、 当某一个单选框选中的时候 v-model 会将当前的 value值 改变 data 中的 数据
hobby 的值就是选中的值,我们只需要实时监控他的值就可以了
-->
<div>
<span>爱好:</span>
<input type="checkbox" id="ball" value="1" v-model='hobby'>
<label for="ball">篮球</label>
<input type="checkbox" id="sing" value="2" v-model='hobby'>
<label for="sing">唱歌</label>
<input type="checkbox" id="code" value="3" v-model='hobby'>
<label for="code">写代码</label>
</div>
<script>
new Vue({
data: {
// 默认会让当前的 value 值为 2 和 3 的复选框选中
hobby: ['2', '3'],
},
})
</script>
获取下拉框和文本框的值
-
通过v-model
-
下拉框(多选框) 中每一个option 必须要有value属性 且value 值不能一样
<div> <span>职业:</span> <!-- 1、 需要给select 通过v-model 双向绑定 一个值 2、 每一个option 必须要有value属性 且value 值不能一样 3、 当某一个option选中的时候 v-model 会将当前的 value值 改变 data 中的 数据 occupation 的值就是选中的值,我们只需要实时监控他的值就可以了 --> <!-- multiple 多选 --> <select v-model='occupation' multiple> <option value="0">请选择职业...</option> <option value="1">教师</option> <option value="2">软件工程师</option> <option value="3">律师</option> </select> <!-- textarea 是 一个双标签 不需要绑定value 属性的 --> <textarea v-model='desc'></textarea> </div> <script> new Vue({ data: { // 默认会让当前的 value 值为 2 和 3 的下拉框选中 occupation: ['2', '3'], desc: 'nihao' }, }) </script>
2.表单修饰符
.number
转换为数值
-
默认表单域中的值为字符串,如果进行数值运算则需要转换成number类型
<!-- 自动将用户的输入值转为数值类型 --> <input v-model.number="age" type="number">
.trim
去除首尾空白
-
自动过滤用户输入的首尾空白字符
-
只能去掉首尾空格,不能去除中间的空格
<!--自动过滤用户输入的首尾空白字符 --> <input v-model.trim="msg">
.lazy
将input事件切换成change事件
-
input事件只有每次输入 时才会触发
-
change事件当失去焦点或按下回车时才会触发
<!-- “change”延迟同步更新 --> <input v-model.lazy="msg" >
3.自定义指令
全局自定义指令(Vue.directive)
- 内置指令不能满足我们的需求时 (一些DOM元素进行底层操作,如聚集输入框)
- Vue允许我们自定义指令
- 使用自定义指令会触发钩子函数
- 自定指令官网 cn.vuejs.org/v2/guide/cu…
当页面加载时,该元素将获得焦点 (注意:autofocus
在移动版 Safari 上不工作)。事实上,只要你在打开这个页面后还没点击过任何内容,这个输入框就应当还是处于聚焦状态。
<!--
使用自定义的指令,只需在对用的元素中,加上'v-'的前缀形成类似于内部指令'v-if','v-text'的形式。
-->
<input type="text" v-focus>
<script>
// 注意点:
// 1、 在自定义指令中 如果以驼峰命名的方式定义 如 Vue.directive('focusA',function(){})
// 2、 在HTML中使用的时候 只能通过 v-focus-a 来使用
// 注册一个全局自定义指令 v-focus
Vue.directive('focus', {
// 当绑定元素插入到 DOM 中。 其中 el为dom元素
inserted: function (el) {
// 聚焦元素el:指令所绑定的元素,可以用来直接操作 DOM。
el.focus();
}
});
new Vue({
el:'#app'
});
</script>
全局自定义指令 (带参)
binding.value
表示的是data数据项el
:指令所绑定的元素,可以用来直接操作 DOM。
<input type="text" v-color='msg'>
<script type="text/javascript">
/*
自定义指令-带参数
bind - 只调用一次,在指令第一次绑定到元素上时候调用
*/
Vue.directive('color', {
// bind声明周期, 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
// el 为当前自定义指令的DOM元素
// binding 为自定义的函数形参 通过自定义属性传递过来的值 存在 binding.value 里面
bind: function(el, binding){
// 根据指令的参数设置背景色
// console.log(binding.value.color)
el.style.backgroundColor = binding.value.color;
}
});
var vm = new Vue({
el: '#app',
data: {
msg: {
color: 'blue'
}
}
});
</script>
局部自定义指令
-
局部指令,需要在vue实例中添加directives 选项, 用法和全局用法一样
-
局部指令只能在当前组件里面使用
-
当全局指令和局部指令同名时以局部指令为准
<input type="text" v-color='msg'> <input type="text" v-focus> <script type="text/javascript"> /* 自定义指令-局部指令 */ var vm = new Vue({ el: '#app', data: { msg: { color: 'red' } }, //局部指令,需要定义在 directives 的选项 directives: { color: { bind: function(el, binding){ el.style.backgroundColor = binding.value.color; } }, focus: { inserted: function(el) { el.focus(); } } } }); </script>
钩子函数
Vue实例从创建到销毁的过程中,会自动调用一些函数,我们称这些函数为钩子函数
一个自定义指令对象可以提供如下几个钩子函数
bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。update
:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。update
:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
钩子函数参数
指令钩子函数会被传入以下参数:
-
el
:指令所绑定的元素,可以用来直接操作 DOM。 -
binding
:一个对象,包含以下 property:name
:指令名,不包括v-
前缀。value
:指令的绑定值,例如:v-my-directive="1 + 1"
中,绑定值为2
。oldValue
:指令绑定的前一个值,仅在update
和componentUpdated
钩子中可用。无论值是否改变都可用。expression
:字符串形式的指令表达式。例如v-my-directive="1 + 1"
中,表达式为"1 + 1"
。arg
:传给指令的参数,可选。例如v-my-directive:foo
中,参数为"foo"
。modifiers
:一个包含修饰符的对象。例如:v-my-directive.foo.bar
中,修饰符对象为{ foo: true, bar: true }
。
-
vnode
:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。 -
oldVnode
:上一个虚拟节点,仅在update
和componentUpdated
钩子中可用。 -
详细说明:参照 cn.vuejs.org/v2/guide/cu…
4.计算属性 ( computed)
计算属性值通过一系列运算之后,最终的到的一个属性值
这个运算出来的属性值可以被模版或methods方法使用
-
插值表达式中放入太多的逻辑会让模板过重难以维护,使用计算属性可以使用模板更简洁
<--!插值表达式中放太多逻辑--> <div id="example"> {{ message.split('').reverse().join('') }} </div>
-
计算属性是基于data的数据实现的,同时也依赖于响应式进行缓存
-
计算属性经常用来处理和模板相关的数据
-
使用
computed
计算属性需要有return返回结果
特点:
- 虽然计算属性在声明的时候被定义为方法,但它的本质是一个属性
- 计算属性会存放计算的结果,只有计算 属性依赖的数据变化时,才会重新进行运算
<div id="app">
<!--
当多次调用 reverseString 的时候
只要里面的 num 值不改变 他会把第一次计算的结果直接返回
直到data 中的num值改变 计算属性才会重新发生计算
-->
<div>{{reverseString}}</div>
<div>{{reverseString}}</div>
<!-- 调用methods中的方法的时候 他每次会重新调用 -->
<div>{{reverseMessage()}}</div>
<div>{{reverseMessage()}}</div>
</div>
<script type="text/javascript">
/*
计算属性与方法的区别:计算属性是基于依赖进行缓存的,而方法不缓存
*/
var vm = new Vue({
el: '#app',
data: {
msg: 'Nihao',
num: 100
},
methods: {
reverseMessage: function(){
console.log('methods')
return this.msg.split('').reverse().join('');
}
},
//computed 属性 定义 和 data 已经 methods 平级
computed: {
// reverseString 这个是我们自己定义的名字
reverseString: function(){
console.log('computed')
var total = 0;
// 当data 中的 num 的值改变的时候 reverseString 会自动发生计算
for(var i=0;i<=this.num;i++){
total += i;
}
// 这里一定要有return 否则 调用 reverseString 的 时候无法拿到结果
return total;
}
}
});
</script>
计算属性与methods方法的区别
- computed计算属性是基于data的响应式依赖进行缓存的,只有相关响应式依赖发生改变时它才会重新求值
- methods方法没有缓存,访问多少次就执行多少次
- 为什么需要计算属性, 减少性能的开销
5.侦听器 watch
- 侦听器 侦听data数据的变化做特定的操作。
- 监听文本输入框绑定的值变化而执行接口请求
- 一般使用在开销比较大或异步任务 比如ajax请求
- watch中的属性一定是data中已经存在的数据
5.1 侦听器简写法
<div id="app">
<input type="text" v-model="username">
<span>{{ msg }}</span>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script src="./lib/axios.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
username: '',
msg: '',
},
watch: {
//去掉了 username函数是去掉了 :function简化写法
//newVal传播新值,oldVal存储旧值,可以省略不写
// username对应的是data里面的 username
//当data里面的 username数据发生改变会触发watch
username(newVal, oldVal) {
.get('https://www.escook.cn/api/finduser/' + newVal).then(result => {
this.msg = result.data.message
})
}
}
})
</script>
5.2 监听数据初始化
<div id="app">
<input type="text" v-model="username">
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
username: '12'
},
watch: {
//需要配置侦听器,则需要写成对象格式,才可以写很多配置项
// username(键:键听数据项) :{侦听器的所有配置写到这里}
username: {
//handeler这个函数名字是固定的,当侦听的数据变化后,handeler函数就会执行
handeler: function (val) {
console.log(val);
},
//immediate,页面加载后,马上侦听一次
immediate: true
}
}
})
</script>
5.3 深度监听对象属性的变化
<div id="app">
<input type="text" v-model="username.age">
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
username:{
age:12
}
},
watch: {
//当对象属性改变,侦听器不会在出发,只有 username对象重新赋值才会触发侦听器
username(val){
console.log(val)
}
-----------------------分割线-----------------
//这种写法,只能用于侦听对象某个属性变化
username.age(val){
console.log(val)
}
-----------------------分割线-------------------
//这个写法,无论对象那个属性改变,都会监听到最新结果
username: {
handler: function (val) {
console.log(val);
},
//深度侦听,当对象中的某个属性改变后,也能侦听到
deep:true
}
}
})
</script>
6.过滤器
-
格式化数据,比如字符串的首字母大写,将日期格式化成指定格式
-
常用于文本的格式化, 过滤器可以使用的两个地方,插值表达式和v-bind属性表达式
-
支持多个过滤器一起使用 (执行顺序是 从左向右)
-
每一个过滤器的功能保持单一 如果有多个过滤需求 申明多个过滤器
-
过滤器应该被添加在JavaScript表达式的尾部
-
过滤器不改变真正的data,而只是改变渲染的结果,并由return返回结果
-
当全局过滤器和局部过滤器重名时,会采用局部过滤器
-
全局注册时是filter,不需要加s的。
-
局部注册时是filters,需要加s
Vue3.0已经删除过滤器,不在支持过滤器 使用计算属性或者methods方法代替
扩展阅读⚠️:vue3js.cn/docs/zh/gui…
6.1 全局过滤器
- Vue.filter()方法接收两个参数
- 第一个,是全局过滤器的“名字”
- 第二个参数是全局过滤器的“处理函数“
<div id="app">
<input type="text" v-model='msg'>
<!-- upper 被定义为接收单个参数的过滤器函数,表达式 msg 的值将作为参数传入到函数中 -->
<div>{{msg | upper}}</div>
<!--
支持级联操作
upper 被定义为接收单个参数的过滤器函数,表达式msg 的值将作为参数传入到函数中。
然后继续调用同样被定义为接收单个参数的过滤器 lower ,将upper 的结果传递到lower中
-->
<div>{{msg | upper | lower}}</div>
<div :abc='msg | upper'>测试数据</div>
</div>
<script type="text/javascript">
// lower 为全局过滤器 名称
Vue.filter('lower', function(val) {
//过滤器逻辑代码
return val.charAt(0).toLowerCase() + val.slice(1);
});
var vm = new Vue({
el: '#app',
data: {
msg: ''
}
});
</script>
6.2 私有过滤器
私有过滤器,只能在当前vm实例控制的el区域内使用
<div id="app">
<input type="text" v-model='msg'>
<!-- upper 被定义为接收单个参数的过滤器函数,表达式 msg 的值将作为参数传入到函数中 -->
<!-- 在花括号中通过“管道符”调用upper过滤器,对msg的值进行格式化 -->
<div>{{msg | upper}}</div>
<!--
支持级联操作
upper 被定义为接收单个参数的过滤器函数,表达式msg 的值将作为参数传入到函数中。
然后继续调用同样被定义为接收单个参数的过滤器 lower ,将upper 的结果传递到lower中
-->
<div>{{msg | upper | lower}}</div>
<!--在 v-bind中通过“管道符”调用 upper过滤,对msg的值进行格式化 -->
<div :abc='msg | upper'>测试数据</div>
</div>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
msg: ''
},
//filters 属性 定义 和 data 已经 methods 平级
// 定义filters 中的过滤器为局部过滤器
filters: {
// upper 自定义的过滤器名字
// upper 被定义为接收单个参数的过滤器函数,表达式 msg 的值将作为参数传入到函数中
upper: function(val) {
// 过滤器中一定要有返回值 这样外界使用过滤器的时候才能拿到结果
return val.charAt(0).toUpperCase() + val.slice(1);
}
}
});
</script>
6.3 过滤器中传递参数
- 使用过滤器时在括号中传递参数
- 从第二个参数开始接过滤器参数
<div id="box">
<!--
filterA 被定义为接收三个参数的过滤器函数。
其中 message 的值作为第一个参数,
普通字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数。
-->
{{ message | filterA('arg1', 'arg2') }}
</div>
<script>
// 在过滤器中 第一个参数 对应的是 管道符前面的数据 n 此时对应 message
// 第2个参数 a 对应 实参 arg1 字符串
// 第3个参数 b 对应 实参 arg2 字符串
Vue.filter('filterA',function(n,a,b){
if(n<10){
return n+a;
}else{
return n+b;
}
});
new Vue({
el:"#box",
data:{
message: "哈哈哈"
}
})
</script>
7.日期格式化规则
<div id="app">
<div>{{date | format('yyyy-MM-dd hh:mm:ss')}}</div>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
/*
过滤器案例:格式化日期
*/
// Vue.filter('format', function(value, arg) {
// 缺点是日期格式单一
// if(arg == 'yyyy-MM-dd') {
// var ret = '';
// ret += value.getFullYear() + '-' + (value.getMonth() + 1) + '-' + value.getDate();
// return ret;
// }
// return value;
// })
Vue.filter('format', function(value, arg) {
//这是一个日期格式化规则
function dateFormat(date, format) {
if (typeof date === "string") {
var mts = date.match(/(/Date((\d+))/)/);
if (mts && mts.length >= 3) {
date = parseInt(mts[2]);
}
}
date = new Date(date);
if (!date || date.toUTCString() == "Invalid Date") {
return "";
}
var map = {
"M": date.getMonth() + 1, //月份
"d": date.getDate(), //日
"h": date.getHours(), //小时
"m": date.getMinutes(), //分
"s": date.getSeconds(), //秒
"q": Math.floor((date.getMonth() + 3) / 3), //季度
"S": date.getMilliseconds() //毫秒
};
format = format.replace(/([yMdhmsqS])+/g, function(all, t) {
var v = map[t];
if (v !== undefined) {
if (all.length > 1) {
v = '0' + v;
v = v.substr(v.length - 2);
}
return v;
} else if (t === 'y') {
return (date.getFullYear() + '').substr(4 - all.length);
}
return all;
});
return format;
}
//调用日期格式化函数
return dateFormat(value, arg);
})
var vm = new Vue({
el: '#app',
data: {
date: new Date()
}
});
</script>
8.生命周期
- 事物从出生到死亡的过程
- Vue实例从创建到销毁的过程,这些过程中会伴随着一些函数的自调用,我们称这些函数为钩子函数
主要阶段
-
挂载(初始化相关属性)
- beforeCreate
- created
- beforeMount
- mounted
-
更新
- beforeUpdate
- updated
-
销毁
- beforeDestroy
- destroyed
常用的钩子函数
beforeCreate | 在实例初始化之后,数据观测和事件配置之前被调用 此时data 和 methods 以及页面的DOM结构都没有初始化 什么都做不了 |
---|---|
created | 在实例创建完成后被立即调用此时data 和 methods已经可以使用 但是页面还没有渲染出来 |
beforeMount | 在挂载开始之前被调用 此时页面上还看不到真实数据 只是一个模板页面而已 |
mounted | el被新创建的vm.$el替换,并挂载到实例上去之后调用该钩子。 数据已经真实渲染到页面上 在这个钩子函数里面我们可以使用一些第三方的 插件 |
beforeUpdate | 数据更新时调用,发生在虚拟DOM打补丁之前。 页面上数据还是旧的 |
updated | 由于数据更改导致的虚拟DOM重新渲染和打补丁,在这之后会调用该钩子。 页面上数据已经替换成最新的 |
beforeDestroy | 实例销毁之前调用 |
destroyed | 实例销毁后调用 |
created函数,可以使用data数据methods方法,可以在此时调用methods方法发送AJax请求
created(){
this.getSt()
},
methods:{
async getSt(){
let result= await axios.get('http://www.itcbc.com:3006/api/getechartsdata')
//结构赋值
let {data}=result.data
this.pie=data.pie
this.line=data.line
}
},
mounted函数中,可以找到页面DOM元素,可以进行DOM操作
激活|失去2个周期函数
-
组件激活会触发
activated
- 激活后,可以立即使用data数据和DOM元素
-
组件失活会触发
deactivated
- 失活后,可以使用data数据,可以通过ref找其子组件实例(如果有的话,但不能获取到DOM元素)
父子组件周期函数的执行顺序
不带缓
创建阶段
父:beforeCreate
父:created
父:beforeMount//先执行父组件的初始化到将数据放到元素里面,还没有呈现页面
子:beforeCreate//在执行子组件的初始化到将数据放到元素里面,渲染页面
子:created
子:beforeMount
子:mounted
父:mounted//最后父组件渲染页面
销毁阶段
父:beforeDestroy//父组件即将销毁,当还可以使用
子:beforeDestroy//子组件即将销毁到已经销毁不可使用
子:destroyed
父:destroyed//父组件销毁不可使用
带缓存
激活阶段
父:beforeCreate
父:created
父:beforeMount
子:beforeCreate
子:created
子:beforeMount
子:mounted
父:mounted//上面到这 和不带缓存创建一样
子:activated --- 多了激活组件
父:activated --- 多了激活组件
失活阶段
子:deactivated//失活组件
父:deactivated//失活组件
errorCaptured周期函数
在捕获一个子组件出现错误被调用,此时钩子会收到三个参数:错误对象,发生错误的组件实例以及一个包含错误来源信息的字符串(因为错误会依次向上传播)
可以使用false阻止该错误继续向上传播
errorCaptured(err, vm, info) {
console.log(err.message) // 只输出后代组件的错误描述信息
console.log(vm) // 发生错误的组件实例
console.log(info) // 一个描述信息
return false // 阻止错误继续向上层传播(错误会从子向父一层层传播)
}
9.数组变异方法
- 在 Vue 中,直接修改对象属性的值无法触发响应式。当你直接修改了对象属性的值,你会发现,只有数据改了,但是页面内容并没有改变
- 变异数组方法即保持数组方法原有功能不变的前提下对其进行功能拓展
- 这些方法都会影响到原始数据 并且会被Vue处理成响应式数据
push() | 往数组最后面添加一个元素,成功返回当前数组的长度 |
---|---|
pop() | 删除数组的最后一个元素,成功返回删除元素的值 |
shift() | 删除数组的第一个元素,成功返回删除元素的值 |
unshift() | 往数组最前面添加一个元素,成功返回当前数组的长度 |
splice() | 有三个参数,第一个是想要删除的元素的下标(必选),第二个是想要删除的个数(必选),第三个是删除 后想要在原位置替换的值 |
sort() | sort() 使数组按照字符编码默认从小到大排序,成功返回排序后的数组 |
reverse() | reverse() 将数组倒序,成功返回倒序后的数组 |
替换数组
- 不会改变原始数组,但总是返回一个新数组
- 这些方法不会改变原始数组,所以不会改变模板中的数据
filter | filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。 |
---|---|
concat | concat() 方法用于连接两个或多个数组。该方法不会改变现有的数组 |
slice | slice() 方法可从已有的数组中返回选定的元素。该方法并不会修改数组,而是返回一个子数组 |
动态数组响应式数据
- Vue.set(vm.items,index,newValue)
- vm.$set(vm.items,index,newValue)
- 参数一表示要处理数组名称
- 参数二表示处理数组的索引
- 参数三表示处理数组后的值
动态对象响应式数据
-
Vue.set(vm.items,index,newValue)
-
vm.$set(vm.items,index,newValue)
- 参数一表示要处理的对象名称
- 参数二表示要处理对象的属性名
- 参数三表示处理对象后的值
10.插槽
使用子组件时,为子组件提供内容的功能
子组件
<template>
< div>
<header>
<!-- slot标签占位符,写在那个表签内,表示这里要方一些内容-->
<!-- 具名插槽 -->
<slot name="header"></slot>
</header>
<main>
<!--默认插槽 -->
<!-- 不带name默认default,所以可以省略不写 -->
<slot></slot>
<!-- 还可以这样写default默认 -->
<slot name="default"></slot>
</main>
<footer>
<!-- 作用域插槽 -->
<slot name="footer" :uname="name"></slot>
</footer>
</div>
<template>
<script>
export default{
//data只样写表返回一个对象
data:()=>({
bame:'12313'
})
}
</script>
父组件
<template>
<my-left>
<!--在使用v-slot没有用<template>包裹,会被视为默认插槽内容 -->
<template v-slot:header>具名</template>
<!-- 默认可以写 v-slot:default也可以不写-->
<template v-slot:default>默认</template>
<template v-slot:footer="user">作用域:{{user.name}}</template>
<!-- v-slot:footer可以简写为#footer-->
<!--但是省略default不可以简写 -->
<template #footer>简写</template>
</my-left>
</template>