Material как WebComponents

от автора

В последнее время, как я уже отмечал в предыдущей статье, вполне себе активно ведутся разработки WebComponents оберток для популярных фреймворков позволяющий использовать их через браузерное API. Это значит, что если вы хотите использовать готовые компоненты созданные на том или ином фреймворке, то вам не обязательно разворачивать проект и собирать его. Также это значит, что вы можете использовать разработки на разных фреймворках связывая их между собой посредством взаимодействия через API браузера.

Не очень давно я пытался найти приличный грид для веб-компонент, на тот момент такового, полноценного, но при том не обязывающего использовать какой-либо фреймворк, особенно если он представляет собой что-то типа Polymer не оказалось. В недалеком прошлом у меня был до того достаточно успешный опыт с material/cdk. Тогда мне относительно легко удавалось серьезно кастомизировать фильтры и пейджер для таблицы, локализовать подсказки и все это без переписывания библиотечного кода или мрачных хуков, использовав механизмы переопределения. На момент рассмотрения оказалось, что биндингов конкретно для компонента таблицы еще не успели сделать, но вот пару недель назад я заметил, что в репозитории на эту тему что-то появилось и решил в рамках эксперимента попробовать подключить их как веб-компоненты.

Для того, что-бы начать пользоваться компонентами material достаточно подключить бандл с кодом и еще один ресурс со всеми стилями, прямо как у любителей VueJS. Например, создать поле ввода управляемое angular/material можно следующим образом:

<link rel="stylesheet" href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css"> <script src="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.js"></script>  <div class="mdc-text-field">    <input type="text" id="my-text-field" class="mdc-text-field__input">    <label class="mdc-floating-label" for="my-text-field">Label</label>    <div class="mdc-line-ripple"></div> </div>  <script>    mdc.textField.MDCTextField.attachTo(document.querySelector('.mdc-text-field')); </script> 

И таблицу для отображения данных мы можем подключить так же запросто, прямо из примера документации по ссылке.

<div class="mdc-text-field">    <input type="text" id="my-text-field" class="mdc-text-field__input">    <label class="mdc-floating-label" for="my-text-field">Label</label>    <div class="mdc-line-ripple"></div> </div> <div class="mdc-data-table">    <table class="mdc-data-table__table" aria-label="Dessert calories">        <thead>        <tr class="mdc-data-table__header-row">            <th class="mdc-data-table__header-cell" role="columnheader" scope="col">Dessert</th>            <th class="mdc-data-table__header-cell" role="columnheader" scope="col">Carbs (g)</th>            <th class="mdc-data-table__header-cell" role="columnheader" scope="col">Protein (g)</th>            <th class="mdc-data-table__header-cell" role="columnheader" scope="col">Comments</th>        </tr>        </thead>        <tbody class="mdc-data-table__content">        <tr class="mdc-data-table__row">            <td class="mdc-data-table__cell">Frozen yogurt</td>            <td class="mdc-data-table__cell mdc-data-table__cell--numeric">24</td>            <td class="mdc-data-table__cell mdc-data-table__cell--numeric">4.0</td>            <td class="mdc-data-table__cell">Super tasty</td>        </tr>        <tr class="mdc-data-table__row">            <td class="mdc-data-table__cell">Ice cream sandwich</td>            <td class="mdc-data-table__cell mdc-data-table__cell--numeric">37</td>            <td class="mdc-data-table__cell mdc-data-table__cell--numeric">4.3</td>            <td class="mdc-data-table__cell">I like ice cream more</td>        </tr>        <tr class="mdc-data-table__row">            <td class="mdc-data-table__cell">Eclair</td>            <td class="mdc-data-table__cell mdc-data-table__cell--numeric">24</td>            <td class="mdc-data-table__cell mdc-data-table__cell--numeric">6.0</td>            <td class="mdc-data-table__cell">New filing flavor</td>        </tr>        </tbody>    </table> </div>  <script src="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.js"></script>  <script type="module">    let filterField = mdc.textField.MDCTextField.attachTo(document.querySelector('.mdc-text-field'));      let dataTable = new mdc.dataTable.MDCDataTable(document.querySelector('.mdc-data-table')); </script>  

Подключая из уже собранных бандлов с unpkg мы к сожалению не используем нативных модульных возможностей браузера WHATWG, т.к. эти бандлы биндят компоненты в глобальное пространство имен и его объект mdc, а не экспортируют по модульному стандарту ES6. Но такой вариант возможно будет привычнее консервативно настроенным специалистам и может заработать без транспиляторов в браузерах наследия.

Список реализованных компонентов можно смотреть вот в этом репозитории.

К сожалению, на данный момент для таблиц в доступное извне апи выставлены только какие-то взаимодействия с чекбоксами и контентом уже отрендеренных рядов.

Однако, мне удалось нагуглить один пример позволяющий получить доступ к пока скрытому от нас апи, через наследование. Кроме того, вам следует знать также об том, что с помощью наработок проекта Angular Elements, вы можете вести разработку компонентов в инфраструктуре фреймворка и самостоятельно экспонировать их в браузерное API и те же CustomElements.

Я взял пример целиком, внеся некоторые исправления, “чтобы заработало” на исходной верстке и было понятно куда ковырять после прочтения статьи. Кода много и теперь он свернут.

Код таблицы

<link rel="stylesheet" href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css">  <div class="mdc-text-field">    <input type="text" id="my-text-field" class="mdc-text-field__input">    <label class="mdc-floating-label" for="my-text-field">Label</label>    <div class="mdc-line-ripple"></div> </div> <div class="mdc-data-table">    <table class="mdc-data-table__table" aria-label="Dessert calories">        <thead>        <tr class="mdc-data-table__header-row">            <th class="mdc-data-table__header-cell" role="columnheader" scope="col">Dessert</th>            <th class="mdc-data-table__header-cell" role="columnheader" scope="col">Carbs (g)</th>            <th class="mdc-data-table__header-cell" role="columnheader" scope="col">Protein (g)</th>            <th class="mdc-data-table__header-cell" role="columnheader" scope="col">Comments</th>        </tr>        </thead>        <tbody class="mdc-data-table__content">        <tr class="mdc-data-table__row">            <td class="mdc-data-table__cell">Frozen yogurt</td>            <td class="mdc-data-table__cell mdc-data-table__cell--numeric">24</td>            <td class="mdc-data-table__cell mdc-data-table__cell--numeric">4.0</td>            <td class="mdc-data-table__cell">Super tasty</td>        </tr>        <tr class="mdc-data-table__row">            <td class="mdc-data-table__cell">Ice cream sandwich</td>            <td class="mdc-data-table__cell mdc-data-table__cell--numeric">37</td>            <td class="mdc-data-table__cell mdc-data-table__cell--numeric">4.3</td>            <td class="mdc-data-table__cell">I like ice cream more</td>        </tr>        <tr class="mdc-data-table__row">            <td class="mdc-data-table__cell">Eclair</td>            <td class="mdc-data-table__cell mdc-data-table__cell--numeric">24</td>            <td class="mdc-data-table__cell mdc-data-table__cell--numeric">6.0</td>            <td class="mdc-data-table__cell">New filing flavor</td>        </tr>        </tbody>    </table> </div>  <script src="https://unpkg.com/material-components-web@latest/dist/material-components-web.js"></script>  <script type="module">    let filterField = mdc.textField.MDCTextField.attachTo(document.querySelector('.mdc-text-field'));     const DATATABLE_COLUMNS_SELECTOR = `.mdc-data-table thead`,        DATATABLE_DATA_SELECTOR = `tbody.mdc-data-table__content`,        DATATABLE_SORTABLE_SELECTOR = `.mdc-data-table--sortable`,        DATATABLE_COLUMNS_NUMERIC = `mdc-data-table--numeric`,        DATATABLE_COLUMNS_SORTABLE = `mdc-data-table--sortable`,        DATATABLE_COLUMNS_SORT_ASC = `mdc-data-table--sort-asc`,        DATATABLE_COLUMNS_SORT_DESC = `mdc-data-table--sort-desc`;     class MyDataTable extends mdc.dataTable.MDCDataTable {         get data() {            return this.foundation_.data;        }         set data(data) {            if (Array.isArray(data)) {                this.foundation_.setData(data);            } else {                throw new Error(`Expected an array`);            }        }         layout() {            if (this.foundation_.layout) {                this.foundation_.layout();            }        }         getDefaultFoundation() {             const getHeaderRow = () => {                let thead = this.root_.querySelector(DATATABLE_COLUMNS_SELECTOR),                    row = thead.querySelector(`tr`);                if (!row) {                    row = document.createElement(`tr`);                    row.setAttribute(`role`, `rowheader`);                    thead.appendChild(row);                }                return row;            },            getHeaderColumns = () => {                return getHeaderRow().querySelectorAll(`th`);            },            emptyHeaderColumns = () => {                getHeaderRow().remove();            },            getData = () => {                return this.root_.querySelector(DATATABLE_DATA_SELECTOR);            },            getDataRows = () => {                return getData().querySelectorAll(`tr`);            },            emptyData = () => {                Array.prototype.map.call(getDataRows(), row => {                    row.remove();                });            };             return new MyDataTableFoundation({                registerSortClickHandler: (handler) => this.root_.addEventListener(`click`, handler),                deregisterSortClickHandler: (handler) => this.root_.removeEventListener(`click`, handler),                // Reads the columns list                readColumns: () => {                    var cols = getHeaderColumns();                    return Array.prototype.map.call(cols, col => {                        return {                            text: col.textContent,                            description: col.getAttribute(`aria-label`),                            numeric: col.classList.contains(DATATABLE_COLUMNS_NUMERIC),                            sortable: col.classList.contains(DATATABLE_COLUMNS_SORTABLE),                            sort: col.classList.contains(DATATABLE_COLUMNS_SORT_ASC) ? 1 : col.classList.contains(DATATABLE_COLUMNS_SORT_DESC) ? -1 : 0                        };                    });                },                // Edit the columns                setColumns: (cols) => {                    emptyHeaderColumns();                    let row = getHeaderRow();                    cols.forEach(col => {                        let column = document.createElement(`th`);                        column.setAttribute(`role`, `columnheader`);                        // Add text                        column.textContent = col.text;                        column.setAttribute(`aria-label`, col.description);                        // Numeric                        if (col.numeric) {                            column.classList.add(DATATABLE_COLUMNS_NUMERIC);                        }                        // Sort                        if (col.sortable) {                            let ariaSort = `none`;                            column.classList.add(DATATABLE_COLUMNS_SORTABLE);                            if (col.sort === `asc` || col.sort === 1) {                                ariaSort = `ascending`;                                column.classList.add(DATATABLE_COLUMNS_SORT_ASC);                            } else if (col.sort === `desc` || col.sort === -1) {                                ariaSort = `descending`;                                column.classList.add(DATATABLE_COLUMNS_SORT_DESC);                            }                            column.setAttribute(`aria-sort`, ariaSort);                        }                        // Add to cols                        row.appendChild(column);                    });                },                // Read data                readData: () => {                    var rows = getDataRows();                    return Array.prototype.map.call(rows, row => {                        let cells = row.querySelectorAll(`td`);                        return Array.prototype.map.call(cells, cell => cell.textContent);                    });                },                // Edit the data                setData: (data) => {                    emptyData();                    let element = getData();                    // Sorting data                    let column = this.columns.find(el => el.sort);                    if (column) {                        let index = this.columns.indexOf(column);                        if (column.sortable) {                            let f = (params => {                                if (params.sort === `desc` || params.sort === -1) {                                    return params.numeric ? (a, b) => b[index] - a[index] : (a, b) => b[index].localeCompare(a[index]);                                } else {                                    return params.numeric ? (a, b) => a[index] - b[index] : (a, b) => a[index].localeCompare(b[index]);                                }                            })(column);                            data.sort(f);                        }                    }                    // For each data                    data.forEach(d => {                        // Create a new row                        let row = document.createElement(`tr`);                        row.setAttribute(`role`, `row`);                        // For each values                        d.forEach((val, i) => {                            // Create a new cell                            let cell = document.createElement(`td`);                            cell.setAttribute(`role`, `gridcell`);                            // Add numeric if needed                            if (this.columns[i].numeric) {                                cell.classList.add(DATATABLE_COLUMNS_NUMERIC);                            }                            // Add content                            if (val instanceof Element) {                                cell.appendChild(val);                            } else {                                cell.textContent = val;                            }                            row.appendChild(cell);                        });                        // Add to cols                        element.appendChild(row);                    });                },                // Redraw data table after edit                redraw: () => {                    this.foundation_.adapter_.setColumns(this.columns);                    this.foundation_.adapter_.setData(this.data);                }            });        }    }     mdc.autoInit.register(`MDCDataTable`, MyDataTable);     class MyDataTableFoundation extends mdc.base.MDCFoundation {         static get defaultAdapter() {            return {                registerSortClickHandler: ( /* handler: EventListener */ ) => {},                deregisterSortClickHandler: ( /* handler: EventListener */ ) => {},                readColumns: () => {},                setColumns: () => {},                readData: () => {},                setData: () => {},                redraw: () => {}            };        }         constructor(adapter) {            super(Object.assign(MyDataTableFoundation.defaultAdapter, adapter));            // Attributes            this.columns = [];            this.data = [];            // Methods            // On sort            this.sortClickHandler_ = (e) => {                let target = e.target.closest(DATATABLE_SORTABLE_SELECTOR);                if (target) {                    let index = Array.prototype.indexOf.call(target.parentElement.children, target);                    this.columns.forEach((col, i) => {                        if (i !== index) {                            col.sort = 0;                        } else {                            if (col.sort === `asc` || col.sort === 1) {                                col.sort = `desc`;                            } else {                                col.sort = `asc`;                            }                        }                    });                    this.adapter_.redraw();                }            };        }         init() {            // Read columns            this.columns = this.adapter_.readColumns();            // Read data            this.data = this.adapter_.readData();            // Click            this.adapter_.registerSortClickHandler(this.sortClickHandler_);        }         destroy() {            // Click            this.adapter_.deregisterSortClickHandler(this.sortClickHandler_);        }         setColumns(cols) {            this.adapter_.setColumns(cols);        }         setData(data) {            this.adapter_.setData(data);        }     }     let dataTable = new MyDataTable(document.querySelector('.mdc-data-table'));  </script> 

Этот пример реализует некоторый минимум для переопределения и экспонирует недостающие в апи методы взаимодействия с апи. Благодаря этому мы можем интегрировать компонент таблицы в недрах фреймворка с другим компонентом поля ввода, добавив свою функциональность фильтрации.

let filterField = mdc.textField.MDCTextField.attachTo(document.querySelector('.mdc-text-field')); filterField.input_.oninput = (event) => {    dataTable.origData = dataTable.origData || dataTable.data.slice();    if (event.target.value == '') {        dataTable.data = dataTable.origData.slice();    } else {        let data = dataTable.origData.filter((row) => {            let rowIsOk = false;            for (let item of row) {                if (item.indexOf(event.target.value) > 0) {                    rowIsOk = true;                }            }            return rowIsOk;        }) || [];        dataTable.data = data;        dataTable.getDefaultFoundation().redraw();    } }; 

Концептуально не очень правильно реализовывать фильтрацию прямо в обработчике события, для этого у нас теперь есть и класс таблицы и более близкие по смыслу структуры фаундейшина и адаптера данных, однако, наша задача на сегодня убедиться в возможности организовать взаимодействие компонентов. И у нас как раз получилось связать два компонента не имеющих общей реализационной логики кодом в контексте выполнения браузера.


после ввода данных в поле, содержимое будет отфильтровано


Этот пример, особенно после вынесения всей логики на джаваскрипте в отдельные файлы-классы, как мы это делали в первой статье цикла, может стать отправной точкой для вас, чтобы переиспользовать компоненты angular/material cdk или другого тулкита для собственной разработки переопределяя поведение, чтобы не “колхозить” все с нуля или интегрируя новый код в существующую инфраструктуру перестав наращивать монолит, т.к. веб-компоненты дают лучший способ модульной организации разработок.


ссылка на оригинал статьи https://habr.com/ru/post/462695/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *