今回作成するのはタイトルをクリックでラジオボタンにチェックし、コンテンツを開くコンポーネントです。
支払い方法の選択等で1つは選択してほしい、かつ説明が必要な時に使用します。
ElementUIのアコーディオンでは満たせなかったのですが、ElementUIベースで作成しました。
http://element.eleme.io/#/en-US/component/collapse
Point
- $refs
- $nextTick
- コンポーネントにv-model
$refsを使うことでDOMへ直接アクセスできます。
$nextTickで更新後のDOMへアクセスできます。
$refs->$nextTickというパターンがよく使われます。
Pay.vueのcollapseのv-modelは親子間のprops、emitをシンプルに定義することができます。
v-modelは一見双方向バインディングしているようで、実際はVueルールに則ってpropsで親から値を渡しeventで子から値を更新しているわけですね。
Pay.vue
<collapse v-model="activeNames">
<collapse-item type="radio" name="pay_type" number="1">
<template slot="title">クレジットカード</template>
<credit></credit>
</collapse-item>
<collapse-item type="radio" name="pay_type" number="2">
<template slot="title">銀行振込</template>
<div>購入後に登録いただいてるメールアドレスへ振込先をお送りします。振込後2,3日でお届けします。</div>
</collapse-item>
<collapse-item type="radio" name="pay_type" number="3">
<template slot="title">仮想通貨</template>
<div>現在仮想通貨での支払いはできません。</div>
</collapse-item>
</collapse>
Collapse.vue
<template>
<div class="radio-collapse">
<slot></slot>
{{value}}
</div>
</template>
<script>
export default {
props: {
accordion: Boolean,
value: {
type: [Array, String],
default() {
return [];
}
}
},
data() {
return {
activeNames: [].concat(this.value)
};
},
watch: {
value(value) {
this.activeNames = [].concat(value);
}
},
methods: {
setActiveNames(activeNames) {
this.activeNames = [].concat(activeNames);
this.$emit('input', activeNames);
},
handleItemClick(item) {
this.setActiveNames(item.number);
}
},
created() {
this.$on('item-click', this.handleItemClick);
}
};
</script>
CollapseItem.vue
<template>
<div class="radio-collapse" :class="{'is-active': isActive}">
<div class="radio-collapse__header" @click="headerClick">
<input type="radio" :name="name" :number="number" :checked="isActive">
<span><slot name="title"></slot></span>
</div>
<div class="radio-collapse__content" ref="content" :style="contentStyle">
<div><slot></slot></div>
</div>
</div>
</template>
<script>
export default {
props: {
name: String,
type: String,
number: String,
},
data() {
return {
contentStyle: {},
contentHeight: 0
};
},
computed: {
isActive() {
return this.$parent.activeNames.indexOf(this.number) > -1;
}
},
watch: {
isActive: function(value) {
value ? this.open() : this.close();
}
},
methods: {
open() {
const contentElm = this.$refs.content;
const contentStyle = this.contentStyle;
contentStyle.display = 'block';
this.$nextTick(_ => {
contentStyle.height = this.contentHeight + 'px';
contentStyle.height = 'auto';
});
},
close() {
const contentElm = this.$refs.content;
const contentHeight = contentElm.clientHeight;
const contentStyle = this.contentStyle;
this.contentHeight = contentHeight;
this.$set(this.contentStyle, 'height', contentHeight + 'px');
this.$nextTick(_ => {
contentStyle.height = '0';
this.$set(this.contentStyle, 'display', 'none');
});
},
init() {
this.contentHeight = this.$refs.content.clientHeight;
if (!this.isActive) {
this.$set(this.contentStyle, 'height', '0');
this.$set(this.contentStyle, 'display', 'none');
}
},
headerClick() {
this.$parent.$emit('item-click', this);
}
},
mounted() {
this.init();
}
}
</script>