select元素是一個很常出現、很常使用也很難客製化的元素。所以我第一篇的實做會選擇select來客製化。為了能更好的複用組件,我會分成以下數個部分:顯示資料用的組件,下拉選單的外框組件,下拉選單的內容組件,選單的箭頭組件,以及把所有組件串起來的組件。
晚一點會把程式碼放上github
<body>
<!-- 雖然使用者看不到select元素,但form還是可以收到select的資料 -->
<form id="form">
<my-single-select>
<select>
<option value="1">1</option>
<option value="2" selected>2</option>
<option value="3">3</option>
</select>
</my-single-select>
<button type="submit">Submit</button>
</form>
<script type="module">
import { MySingleSelect } from "./select.js";
customElements.define("my-single-select", MySingleSelect);
const FormNode = document.getElementById("form");
FormNode.addEventListener("submit", (e) => {
e.preventDefault();
console.log(FormNode.elements[0].value);
});
</script>
</body>
這個組件就是顯示選單框的文字用的組件
class MySingleSelectContent extends HTMLElement {
constructor() {
super()
this.attachShadow({mode: 'open'})
this.shadowRoot.innerHTML = `
<style>
:host {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 5px;
}
</style>
<slot></slot>
`
}
}
這個組件就是下拉選單的那個選單
class MySelectDataList extends HTMLElement {
constructor() {
super()
this.attachShadow({mode: 'open'})
this.shadowRoot.innerHTML = `
<style>
:host {
position: absolute;
top: 100%;
left: 0;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #ffffff;
width: 100%;
}
.container {
display: flex;
flex-direction: column;
width: 100%;
min-width: 20px;
max-height: 300px;
overflow: auto;
}
</style>
<div class="container">
<slot></slot>
</div>
`
}
}
這個組件就是下拉選單的那個選項
class MySingleSelectOption extends HTMLElement {
constructor() {
super()
this.attachShadow({mode: 'open'})
this.shadowRoot.innerHTML = `
<style>
:host {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 5px;
height: 3rem;
width: 100%;
}
</style>
<slot></slot>
`
}
}
這個組件就是下拉選單的箭頭組件
class MySelectArrow extends HTMLElement {
constructor() {
super()
this.attachShadow({mode: 'open'})
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid #000;
}
</style>
`
}
}
組件本身,因為程式碼有點多,只列出關鍵的部分。
生命周期相關
// 這裡就只有簡單的打開選單,關上選單,和每個選項選擇時使用this.selectOption(option)事件
connectedCallback() {
this.selectContentNode.addEventListener('click', () => {
this.handleSelectOpen()
})
this.maskNode.addEventListener('click', () => {
this.handleSelectClose()
})
for (const option of this.optionMap.keys()) {
option.addEventListener('click', this.selectOption(option))
}
}
disconnectedCallback() {
this.selectContentNode.removeEventListener('click', () => {
this.handleSelectOpen()
})
this.maskNode.removeEventListener('click', () => {
this.handleSelectClose()
})
for (const option of this.optionMap.keys()) {
option.removeEventListener('click', this.selectOption(option))
}
}
渲染的函式
render() {
// 因為每個部分都拆成組件,最好先確認是否註冊組件
if (!customElements.get('my-select-content') ) {
customElements.define("my-select-data-list", MySelectDataList);
}
if (!customElements.get('my-select-mask') ) {
customElements.define("my-select-mask", MySelectMask);
}
if (!customElements.get('my-single-select-content') ) {
customElements.define("my-single-select-content", MySingleSelectContent);
}
if (!customElements.get('my-single-select-option') ) {
customElements.define("my-single-select-option", MySingleSelectOption);
}
if (!customElements.get('my-select-arrow') ) {
customElements.define("my-select-arrow", MySelectArrow);
}
// 這段就生成node,加上class再放入父節點
const container = document.createElement('div');
container.classList.add('container')
this.selectContentNode = document.createElement('my-single-select-content');
this.selectContentNode.classList.add('select-content')
container.appendChild(this.selectContentNode)
this.selectArrow = document.createElement('my-select-arrow');
this.selectArrow.classList.add('select-arrow')
container.appendChild(this.selectArrow)
this.dataListNode = document.createElement('my-select-data-list');
this.dataListNode.classList.add('select-list')
// 這二行是用來把select裡的option的資料用web component替換後放入組件中
const originOptions = this.querySelectorAll('option')
this.setOptions(originOptions)
container.appendChild(this.dataListNode)
this.maskNode = document.createElement('my-select-mask');
this.maskNode.classList.add('select-mask')
container.appendChild(this.maskNode)
return container;
}
其他函式
// 把每一個option元素的屬性都抽出來,再套用到自製的選項組件上
setOptions(options) {
this.dataListNode.innerHTML = '';
for (let item of options) {
const attrs = item.attributes
const option = document.createElement('my-single-select-option');
for (let attr of attrs) {
// 對自製的option組件,id和class是多餘的
if (attr.name !== 'id' || attr.name !== 'class') {
option.setAttribute(attr.name, attr.value)
}
// 如果有selected屬性,就要顯示在自製的組件中
if (attr.name === 'selected') {
this.setContent(item.innerHTML)
}
}
option.innerHTML = item.innerHTML
this.optionMap.set(option, item)
this.dataListNode.appendChild(option)
}
}
// 當點擊了一個自製的選項組件,就要修改對應的option組件
selectOption(option) {
return (e) => {
const item = this.optionMap.get(option)
option.selected = true
item.selected = true
this.setContent(option.innerHTML)
this.handleSelectClose()
}
}
// 在自製組件上顯示己經選擇的資料
setContent(value) {
this.selectContentNode.innerHTML = value
}