开发者可以将页面内的功能模块抽象成自定义组件,以便在不同的页面中重复使用;也可以将复杂的页面拆分成多个低耦合的模块,有助于代码掩护。自定义组件在使用是与基础组件非常相似。
(自定义组件需要重新开一个文件夹,然后再在其他页面引用,可以理解成封装一个组件,一个组件放在一个文件夹里)类似于页面,一个自定义组件由 json wxml wxss js 四个文件组成。要编写一个自定义组件,首先需要在 json 文件中进行自定义组件声明(将 component 字段设为 true 可将这一组件设为自定义组件):
{
"component": true
}
同时,还要 wxml 文件中编写组件模板,在 wxss 文件中加入组件样式,他们的写法与页面的写法类似。具体细节和注意事项参见 。
<view class="inner">{
{innerText}}view>
<slot>slot>
/* 这里的样式只应用于这个自定义组件 */
.inner{
color:red;
}
在自定义组件的js文件中,需要使用 Component()来注册组件,并提供组件的属性定义,内部数据和自定义方法。
组件的属性值和内部数据将被用于组件 wxml 的渲染,其中,属性值是可由组件外部传入的。更多细节参见 Component构造器。
Component({
properties:{
//这里定义了innerText属性,属性值可以在组件使用时指定
innerText:{
type:String,
value:'default value',
}
},
data:{
//这里是一些组件内部数据
someData:{
}
},
methods:{
// 这是一个自定义方法
customMethod:function(){
}
}
})
使用已注册的自定义组件前,首先要在页面的json 文件中进行应用声明。此时需要提供每个自定义组件的标签名和对应的自定义组件文件路径:
//需要应用的页面json文件
{
"usingComponents":{
"component-tag-name":"path/to/the/custom/component"
//前面是组件名,后面是封装的文件的地址
}
}
这样,在页面的 wxml 中就可以像使用基础组件一样使用自定义组件。节点名即自定义组件的标签名,节点属性即传递给组件的属性值。
<view>
<component-tag-name inner-text="Some text">component-tag-name>
view>
(微信开发者工具有一个代码示例片段)
自定义组件的 wxml 节点结构在与数据结合之后,将被插入到引用位置内。
一些需要注意到细节:
注意,是否在页面文件中使用 usingComponents 会使得页面的 this 对象的原型稍有差异 ,
包括:
如果页面比较复杂,新增或删除 usingComponents 定义段时建议重新测试一下。
类似于页面,自定义组件拥有自己的 wxml 模板和 wxss 样式。
组件模板的写法与页面模板相同。组件模板于组件数据结合后生成的节点树,将被插入到组件的引用位置上。
在组件模板中可以提供一个 < slot > 节点,用于承载组件引用时提供的子节点,用于承载组件引用时提供的子节点。
<view class=“wrapper”>
<view>这里是组件的内部节点view>
<slot>slot>
view>
<view>
<component-tag-name>
<view>这里是插入到组件 slot 中的内容view>
component-tag-name>
view>
注意,在模板中引用到的自定义组件及其对应的节点名需要在 json 文件中显式定义,否则会被当作一个无意义的节点,除此以外,节点名也可以被声明为抽象节点。
与普通的 wxml 模板类似,可以使用数据绑定,这样就可以向子组件的属性传递动态函数。
<view>
<component-tag-name prop-a=“{
{dataFieldA}}” porp-b=“{
{dataFieldB}}”>
<view>这里是插入到组件slot中的内容view>
component-tag-name>
view>
在以上例子中,组件的属性 propA 和 propB 将收到页面传递的数据。页面可以通过 setData 来改变绑定的数据字段。
注意:这样的数据绑定只能传递 JSON 兼容数据。还可以在数据中包含函数(但这些函数不能在 WXML 中直接调用,只能传递给子组件)。
在组件的 wxml 中可以包含 slot 节点,用于承载组件使用者提供的 wxml 结构。
默认情况下,一个组建的 wxml 中只能有一个 slot。需要使用多slot时,可以在组件 js 中声明启动。
Component({
options:{
multipleSlots:true //在组件定义时的选项中启用多slot支持
},
properties:{
/* ... */},
methods:{
/* ... */}
})
此时,可以在这个组件的 wxml 中使用多个 slot,以不同 name 来区分。
<view class="wrapper">
<slot name="before">slot>
<view>这里是组件的内部组件view>
<slot name="after">slot>
view>
使用时,用 slot 属性来将节点插入到不同的 slot 上。
<view>
<component-tag-name>
<view slot="before">这里是插入到组件slot name=“defore” 中的内容view>
<view slot="after">这里是插入到组件slot name=“after” 中的内容view>
component-tag-name>
view>
组件对应 wxss 文件的样式,只为组件 wxml 内的节点生效。编写组件样式时,需要注意以下
#a {
} /*在组件中不能使用*/
[a] {
} /*在组件中不能使用*/
button {
} /*在组件中不能使用*/
.a>.b {
} /*除非 .a 是 view 组件节点,否则不一定会生效 */
除此以外,组件可以指定他所在节点的默认样式,使用 :host 选择器
/* 组件 custom-component.wxss */
:host {
color: yellow;
}
<csutom-component>这段文本是黄色的csutom-component>
默认情况下,自定义组件的样式只受到自定义组件 wxss 影响。除非以下两种情况:
Component({
option:{
styleIsolation: 'isolated'
}
})
styleIsolation ,支持以下取值:
如果这个 Component 构造器用于构造页面,则默认值为 shared ,且还有以下的几个选项:
在Component 的 options 中设置 addGlobalClass :true 。这个选项等价于设置 styleIsolation:apply-shared ,但设置了 styleIsolation 选项后这个选项会失效。
/* 组件 custom-component.js*/
Component({
options:{
addGlobalClass:true,
}
})
<text class="red-text">这段文本的颜色由‘app.wxss’和页面‘wxss’中的样式定义来决定text>
/* app.wxss*/
.red-text {
color:red;
}
有时,组件希望接受外部传入的样式类。此时可以在 Component 中用 wxternaClasses 定义段定义若干个外部样式类。
这个特性可以用于现实类似于 view 组件的 hover-class 属性:页面可以提供一个样式类,赋予 view 的 hover-class ,这个样式类本身写在页面中而非 view 组件的实现中。
/* 组件 custom-component.js*/
Component({
externalClasses:['my-class']
})
<custom-component class="my-class">这段文本的颜色由组件外的 class 决定custom-component>
这样,组件的使用者可以指定这个样式类对应的 class ,就像使用普通属性一样。
<custom-component my-class="red-text" />
<custom-component my-class="large-text" />
<custom-component my-class="red-text largen-tex" />
.red-text{
color:red;
}
.large-text{
font-size: 1.5em;
}
Component 构造器可用于定义组件。调用 Component 构造器时可以指定组件的属性,数据,方法等。
Component({
behaviors:[],
properties:{
myPropertty:{
// 属性名
type:string,
value:''
},
myProperty2: String //简化的定义方法
},
data:{
}, //私有数据,可用于模板渲染
lifetimes:{
//生命周期函数,可以为函数,或一个在 methods 段中定义的方法名。
attached: function () {
},
moved: function () {
},
detached: function () {
},
},
//生命周期函数,可以为函数,或一个在 methods 段中定义的方法名
attached:function (){
},//此处 attache 的声明会被lifetime 字段中的声明覆盖
ready:function (){
},
pageLifetiimes:{
show:function () {
},
hide:function () {
},
resize: function () {
},
},
methods:{
onMyButtonTap:function (){
this.setData({
//更新属性和数据的方式于更行页面数据的方法类似
})
},
//内部方法建议以下划线开头
_myPrivateMethod:function(){
//这里将 data.A[0].B 设为 ‘myPrivateData’
this.setData({
'A[0].B':'myPrivateData'
})
},
_propertyChange:function(newVal,oldVal){
}
}
})
事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用 Component 构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应 json 文件中包含 usingComponent 定义段。
此时,组件的属性可以用于接受页面的参数,如访问页面 /page/index/index?paramA=123¶mB=xyz
,如果声明有属性 paramA 或 paramB,则他们会被赋值为 123 或者 xyz 。
页面的声明周期方法 (即 on 开头的方法),应写在 methods 定义段中。
Component({
properties:{
paramA:Number,
paramB:String,
},
methods:{
onLoad:function(){
this.data.paramA //页面参数 paramA 的值
this.data.paramB //页面参数 paramB 的值
}
}
})
使用 Component 构造器来构造页面的一个好处是可以使用 behavios 来提取所有页面中公用的代码段。
例如,在所有页面被创建和销毁时都要执行同一段代码,就可以把这段代码提取到 behaviors 中。
//page-common-behavior.js
module.exports = Behavior({
attached:function (){
console.info('Page loaded!')
},
detached:function() {
console.info('Page unloaded!')
}
})
// 页面 A
var pageCommonBehavior = require('./page-commmon-behavior')
Component({
behaviors:[pageCommonBehavior],
data:{
/* ... */ },
methods:{
/* ... */ },
})
//页面 B
var pageCommonBehavior = require('./page-common-behavior')
Component({
behaviors:[pageCommonBehavior],
data:{
/* ... */ },
methods:{
/* ... */ },
})
组件间的基本通信方式。
事件系统时组件间通信的主要方式之一。自定义组件可以触发任意的事件,引起组件的页面可以监听这些事件。
<component-tag-name bindmyevent="onMyEvent" />
<component-tag-name bind:wdmyevent="onMyEvent" />
Page({
onMyEvent:function(e){
e.detail //自定义组件触发事件时提供的datail对象
}
})
自定义组件触发事件时,需要使用 triggerEvent 方法,指定事件名,detail对象和事件选项:
<button bindTop="onTap">点击这个按钮将触发“myevent”事件button>
Component({
properties:{
},
methods:{
onTap:function(){
var myEventDetail = {
}//detail对象,提供给事件监听函数
var myEventOption = {
}//触发事件的选项
this.triggerEvent('myevent',myEventDetail,myEventOption)
}
}
})
后面例子
选项名 | 描述 |
---|---|
bubbles | 事件是否冒泡 |
composed | 事件是否可以穿越组件边界,为 false 时,事件将只能在引用组件的节点鼠树上触发,不进入其他任何组件内部 |
capturePhase | 事件是否拥有捕获阶段 |
// 页面 page.wxml
<another-component bindcustomevent="pageEventListener1">
<my-component bindcustomevent="pageEventListener2">my-component>
another-component>
//组件 another-component.wxml
<view bindcustomevent="anotherEventListener">
<slot />
view>
//组件 my-component.wxml
<view bindcustomevent="myEventListener">
<slot />
view>
triggerEvent 类似于 addEventListner 后面是可选参数
//组件 my-component.js
Component({
methods:{
onTap:function(){
this.triggerEvent('customevent',{
})//只会触发 pageEventListener2
this.triggerEvent('customevent', {
}, {
bubbles: true }) // 会依次触发 pageEventListener2 、 pageEventListener1
this.triggerEvent('customevent', {
}, {
bubbles: true, composed: true }) // 会依次触发 pageEventListener2 、 anotherEventListener 、 pageEventListener1
}
}
})
组件的生命周期,指的是组件自身的一些函数,这些函数在特殊的事件点或遇到一些特殊的框架事件是被自动触发。
其中,最重要的生命周期是 created attached detached ,包含一个组件实例生命流程的是主要时间点。
生命周期方法可以直接定义再== Component 构造器的第一级参数中,也可以在 lifetimes 字段内进行声明 (这是推荐的方式,其优先级最高)==
Component({
lifetimes:{
attched:function() {
//在组件实例进入页面节点树时执行
},
detached: function() {
//在组件实例被从页面节点数移除时执行
},
},
//以下时旧式的定义方式
attched: function(){
//在组件实例进入页面节点数时执行
},
detached:function() {
//在组件实例被从页面节点树移除时执行
},
.....
})
在 behaviors 中也可以编写生命周期方法,同时不会与其他 behaviors 中的同名生命周期相互覆盖。但要注意,如果一个组件多次直接或间接引用同一个 behavior ,这个 behavior 中的生命周期函数在一个执行时机内只会执行一次。
可用的全部生命周期如下表所示
生命周期 | 描述 |
---|---|
created | 组件实例刚刚被创建时执行 |
attached | 在组件实例进入页面节点树时执行 |
ready | 在组件的视图层布局完成后执行 |
moved | 在组件实例被移动到节点树另一个位置时执行 |
datached | 在组件实例被从页面节点树移除时执行 |
error | 每当组件方法抛出错误时执行 |
还有一些特殊的生命周期。它们并非与组件有很强的关联,但有时组件需要获知,以便组件你内部处理。这样的生命周期称为“组件所在页面的生命周期”,在 pageLifetimes 定义段中定义。其中可用的生命周期包括:
生命周期 | 描述 |
---|---|
show | 组件所在的页面被展示时执行 |
hide | 组件所在的页面被隐藏时执行 |
reszie | 组件所在的页面尺寸变化时执行 |
Component({
pageLiftimes:{
//页面被展示
},
hide:function() {
//页面被隐藏
},
resize: function(size){
//页面尺寸变化
}
})
behaviors 是用于组件间代码共享的特性,类似于一些编程语言中的 “mixins”或“traits”。
每个 behavior 可以包含一组属性,数据,生命周期函数和方法。组件引用它时,它的属性,数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。每个组件可以引用多个 behavior ,behavior 也可以引用其他 bahavior 。
组件引用时,在 behaviors 定义段中将他门逐个列出
// my-component.js
var myBehavior = require('my-behavior')
Component({
behaviors:[myBehavior],
properties:{
myProperty:{
type:String
}
},
data:{
myData:{
}
},
attached: function(){
},
methods:{
myMethod: function(){
}
}
})
在上例中,my-component 组件定义中加入了 my-behavior ,而 my-behavior 中包含有 myBehaviorProperty 属性 ,myBehaviorData 数据字段,myBehaviorMethod 方法和一个 attached 生命周期函数 。
这将使得 my-component 中最终包含myBehaviorPorperty,myProperty两个属性, myBehaviorData,myData 两个数据字段,和myBehaviorMethod,myMetho 两个方法。
当组件触发 attached 生命周期时,会依次触发 my-behavior 中的 attched 生命周期函数和my-component 中的 attached 生命周期函数。
组件和它引用的 behavior 中可以包含同名的字段,对这些字段的处理方法如下:
自定义组件可以通过引用内置的 behavior 来获得内置组件的一些行为。
Component({
behaviors:['wx://form-field']
})
在上例中,wx://form-field 代表一个内置 behavior ,他使得这个自定义组件有类似于表单控件的行为。
内置 behavior 往往会为组件添加一些属性。在没有特殊说明时,组件可以覆盖这些属性来改变它的 type 或添加 observer 。
使自定义组件有类似与表单控件的行为。form 组件可以识别这些自定义组件,并在 submit 事件中返回组件的字段名及其对应字段值,这将为它添加以下两个属性。
属性名 | 描述 |
---|---|
name | 在表单中字段名 |
value | 在表单中的字段值 |
这个定义段可以用于指定组件被 selectComponent 调用时的返回值。
未使用这个定义段时,selectComponent 将返回自定义组件的 this (插件的自定义组件将返回 null )。使用这个定义段时,将以这个定义段的函数返回值代替。
//自定义组件 my-component 内部
Component({
behaviors:['wx://component-export'],
export(){
return {
myField:'myValue'}
}
})
<my-component id="the-id" />
this.selectComponent('#the-id')//等于{myField:‘myValue’}
定义和使用组件间关系
<custom-ul>
<custom-li> item 1 custom-li>
<custom-li> item 2 custom-li>
<custom-ul>
这个例子中, custom-ul 和 custom-li 都是自定义组件,它们有相互间的关系,相互间的通信往往比较复杂。此时在组件定义时加入 relations 定义段,可以解决这样的问题。示例:
// path/to/custom-ul.js
Component({
relations: {
'./custom-li': {
type: 'child', // 关联的目标节点应为子节点
linked: function(target) {
// 每次有custom-li被插入时执行,target是该节点实例对象,触发在该节点attached生命周期之后
},
linkChanged: function(target) {
// 每次有custom-li被移动后执行,target是该节点实例对象,触发在该节点moved生命周期之后
},
unlinked: function(target) {
// 每次有custom-li被移除时执行,target是该节点实例对象,触发在该节点detached生命周期之后
}
}
},
methods: {
_getAllLi: function(){
// 使用getRelationNodes可以获得nodes数组,包含所有已关联的custom-li,且是有序的
var nodes = this.getRelationNodes('path/to/custom-li')
}
},
ready: function(){
this._getAllLi()
}
})
// path/to/custom-li.js
Component({
relations: {
'./custom-ul': {
type: 'parent', // 关联的目标节点应为父节点
linked: function(target) {
// 每次被插入到custom-ul时执行,target是custom-ul节点实例对象,触发在attached生命周期之后
},
linkChanged: function(target) {
// 每次被移动后执行,target是custom-ul节点实例对象,触发在moved生命周期之后
},
unlinked: function(target) {
// 每次被移除时执行,target是custom-ul节点实例对象,触发在detached生命周期之后
}
}
}
})
有时,需要关联的是一类组件
<custom-form>
<view>
input
<custom-input>custom-input>
view>
<custom-submit> submit custom-submit>
custom-form>
custom-form 组件想要关联 custom-input 和 custom-submit 两个组件。此时,如果这两个组件都有同一个 behavior;
// path/to/custom-form-controls.js
module.exports = Behavior({
// ...
})
// path/to/custom-input.js
var customFormControls = require('./custom-form-controls')
Component({
behaviors: [customFormControls],
relations: {
'./custom-form': {
type: 'ancestor', // 关联的目标节点应为祖先节点
}
}
})
// path/to/custom-submit.js
var customFormControls = require('./custom-form-controls')
Component({
behaviors: [customFormControls],
relations: {
'./custom-form': {
type: 'ancestor', // 关联的目标节点应为祖先节点
}
}
})
则在 relation 关系定义中,可使用这个behavior 来代替组件路径作为关联的目标节点:
// path/to/custom-form.js
var customFormControls = require('./custom-form-controls')
Component({
relations: {
'customFormControls': {
type: 'descendant', // 关联的目标节点应为子孙节点
target: customFormControls
}
}
})
relations 定义段包含目标组件路径及其对应选项,可包含的选项见下表。
选项 | 描述 |
---|---|
type | 目标组件的相对关系,可选的值为 parent 、 child 、 ancestor 、 descendant |
linked | 关系生命周期函数,当关系被建立在页面节点树中时触发,触发时机在组件attached生命周期之后 |
linkChanged | 关系生命周期函数,当关系在页面节点树中发生改变时触发,触发时机在组件moved生命周期之后 |
unlinked | 关系生命周期函数,当关系脱离页面节点树时触发,触发时机在组件detached生命周期之后 |
target | 如果这一项被设置,则它表示关联的目标节点所应具有的behavior,所有拥有这一behavior的组件节点都会被关联 |
数据监听器可以用于监听和响应任何属性和数据字段的变化
有时,在一些数据字段别setData设置时,需要执行一些操作。
例如, this.data.sum 永远是 this.data.numberA 与 this.data.numberB 的和。
Component ({
attached: function(){
this.setData({
numberA:1,
numberB:2,
})
},
onservers:{
'numberA,numberB': function(numberA,numberB){
//在 numberA 或者 numberB 被设置时,执行这个函数
this.setData({
sum: numberA + numberB
})
}
}
})
数据监听器支持监听属性或内部数据的变化,可以同时监听多个。一次 setData 最多触发每个监听器一次。
同时,监听器可以监听组函数字段
Component({
observers:{
'some.subfield':function(subfield){
//使用 setData 设置 this.data.some 也会触发
//(除此之外,使用 setData 设置 this.data.some 也会触发)
subfield === this.data.some.subfield
},
'arr[12]': function(arr12){
//使用 setData 设置 this.data.arr[12] 时触发
//(除此以外,使用 setData 设置 this.data.arr 也会触发)
arr12 === this.data.arr[12]
},
}
})
如果需要监听所有子数据字段的变化,可以使用通配符 **
Component({
observers:{
'some.field.**': function (field){
// 使用 setData 设置 this.data.some.field 本身或其下任何子数据字段时触发
// (除此以外,使用 setData 设置 this.data.some 也会触发)
field === this.data.some.field
},
},
attached: function() {
//这样也会触发上面的 observe
this.setData({
'some.field':{
/* ... */}
})
//这样也会触发上面的 observer
this.setData({
'some.field.xxx':{
/* ... */}
})
// 这样还是会触发上面的 observer
this.setData({
'some':{
/* ... */}
})
}
})
特别的,仅使用通配符 ** 可以监听全部 setData
Compoennt({
observers: {
'**':function(){
//每次 setData 都触发
},
},
})
Bugs & Tips :
纯数据字段是一些不用于界面渲染的 data 字段,可以用于提升页面更新性能。
有些情况下,某些 data 中的字段 (包括 setData 设置的字段) 既不会展示在其他页面上,也不会传递给其他组件,仅仅在当前组件内部使用。
此时,可以指定这样的数据字段为“纯数字字段” ,它们将仅仅被记录在 this.data 中,而不参与任何界面渲染过程,这样有助于提高页面更新性能。
指定 “纯数据字段” 的方法是在 Component 构造器的 options 定义段中指定 pureDataPattern 为一个正则表达式,字段名符合这个正则表达式的字段将成为纯字段。
Component({
options:{
pureDataPattern: /^_/ //指定所有 _ 开头的数据字段为纯数据字段
},
data:{
a:true, //普通数据字段
_b:true, //纯数据字段
},
methods:{
myMethod(){
this.data._b //纯数据字段可以在 this.data 中获取
this.setData({
c:true, //普通数据字段
_d: true, //纯数据字段
})
}
}
})
上述组件中的纯数据字段不会别应用到 WXML 上:
<view wx:if="{
{a}}"> 这行会被展示 <view>
<view wx:if="{
{_b}}"> 这行不会被展示<view>
属性也可以被指定为纯数据字段,(遵循 pureDataPattern 的正则表达式)。
属性中的纯数据字段可以像普通属性一样接收传入的属性值,但不能将它直接用于组件自身的 WXML 中。
Component({
options:{
pureDataPattern: /^_/
},
properties:{
a:Boolean,
_b:{
type: Boolean,
_b:{
type:Boolean,
observer(){
//这样做的话,这个observer 永远不会触发
}
},
}
}
})
注意:属性中的纯数据字段的属性 oberver 永远不会触发 , 如果想要监听属性值变化,使用 数据监听器 代替。
数据监听器 可以用于监听纯数据字段(与普通数据字段一样)。这样,可以通过监听,响应纯数据字段的变化来改变界面。
实例:将 javascript 时间戳转化为可读时间的自定义组件。
Component({
options:{
pureDataPattern: /^timestamp$/ //将 timestamp 属性指定为纯数据字段
},
properties:{
timestamp: Number,
},
observes:{
timestamp: function () {
//timeStamp 被设置时,将它展示为可读时间字符串
var timestring = new Date(this.data.timestamp).toLocaleString()
this.setData({
timeString: timeString
})
}
}
})
<view>{
{timeString}}view>
有时,自定义组件模板中的一些节点,其对应的自定义组件不是由自定义组件本身确定的,而是自定义组件的调用者确定的/这时可以把这个节点声明为 “抽象节点”。
例如,我们现在来实现一个“选框组”(selecttable-group) 组件,它其中可以放置单选框或者复选框。
<!-- selecttable-group.wxml -->
<view wx:for="{
{labels}}">
<label>
<selecttable disabled="{
{false}}"></selectable>
{
{
item}}
</label>
</view>
其中,“selectable” 不是任何在 json 文件的 usingComponent 字段中声明的组件,而是一个抽象节点。它需要在 componentGenerics 字段中声明:
{
"componentGenerics":{
"selectable":true
}
}
使用包含抽象节点的组件
在使用 selectable-group 组件时,必须指定“selectable” 具体是那个组件:
<selectable-group generic:selectable="custom-radio" />
这样,在生成这个 selected-group 组件的实例时,“selectable”节点会生成“custom-radio”组件实例。类似地,如果这样使用:
<selectable-group generic:selectable="custom-checkbox" />
“selectable”节点则会生成“custom-checkbox”组件实例。
注意:上述的 custom-radio 和 custom-checkbox 需要包含在这个 wxml 对应 json 文件的 usingComponents 定义段中。
{
"usingComponents": {
"custom-radio": "path/to/custom/radio",
"custom-checkbox": "path/to/custom/checkbox"
}
}
抽象节点可以指定一个默认组件,当具体组件未被指定时,将创建默认组件的实例。默认组件可以在 componentGenerics 字段中指定:
{
"componentGenerics": {
"selectable": {
"default": "path/to/default/component"
}
}
}