iT邦幫忙

2022 iThome 鐵人賽

DAY 21
0
Modern Web

web component - 次世代網頁技術的重要拼圖系列 第 21

web component 的實做- 單項下拉選單組件

  • 分享至 

  • xImage
  •  

select元素是一個很常出現、很常使用也很難客製化的元素。所以我第一篇的實做會選擇select來客製化。為了能更好的複用組件,我會分成以下數個部分:顯示資料用的組件,下拉選單的外框組件,下拉選單的內容組件,選單的箭頭組件,以及把所有組件串起來的組件。

晚一點會把程式碼放上github

目標

主思路

  1. 用組件把select元素給包起來,這樣能保證SSR時還有select元素顯示。
  2. 不使用slot,而是把select元素中所有的option元素取出,clone所有的屬性後換成選項組件。
  3. 當點擊選項組件時,也把對應的option元素修改selected屬性,這樣就能從select元素取得當前的資料,web component本身可以不用實做那些為了取值而必需設定的屬性。

這篇文章沒有完成的部分

  1. 還沒有處理optiongroup元素的部分
  2. 還沒有時間改成headless的無UI組件

實做

實際使用

<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
}

上一篇
web component 的實做- 推薦的最佳實踐方式和個人心得
下一篇
web component 的實做- 多選下拉選單組件
系列文
web component - 次世代網頁技術的重要拼圖30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言