Material as WebComponents

Recently, as I noted in a previous article , WebComponents of wrappers for popular frameworks that allow using them through the browser API are actively developing themselves. This means that if you want to use ready-made components created on a particular framework, then you do not have to deploy the project and assemble it. This also means that you can use the development on different frameworks by linking them together through interaction through the browser API.



Not so long ago I tried to find a decent grid for web components, at that time such a full-fledged one, but at the same time not obliging to use any framework, especially if it was something like Polymer wasn’t. In the recent past, I had a fairly successful experience with material / cdk before. Then it was relatively easy for me to seriously customize the filters and pager for the table, to localize the hints and all this without rewriting the library code or gloomy hooks, using redefinition mechanisms. At the time of the review, it turned out that binders specifically for the table component had not yet been done, but a couple of weeks ago I noticed that something appeared in the repository on this topic and decided to try connecting them as web components as part of the experiment.



In order to start using the components of material, just connect the bundle with the code and another resource with all the styles, just like VueJS fans. For example, you can create an input field controlled by angular / material as follows:



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





And we can connect a table for displaying data just as easily, directly from the sample documentation on the link .



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





When connecting from unpkg bundles with unpkg that are already built, we unfortunately do not use the native modular capabilities of the WHATWG browser, as these bundles bind components to the global namespace and its mdc object, rather than exporting it using the modular ES6 standard. But this option will probably be more familiar to conservatively minded specialists and can work without transporters in legacy browsers.







A list of implemented components can be found in this repository .



Unfortunately, at the moment, only some kind of interaction with the checkboxes and the content of already rendered series is exhibited for tables in an accessible from outside API.



However, I managed to google one example that allows you to access the API so far hidden from us, through inheritance. In addition, you should also be aware that with the help of the Angular Elements project, you can develop components in the framework infrastructure and independently expose them to the browser API and the same CustomElements.







I took the whole example, making some corrections, “to work” on the original layout and it was clear where to pick after reading the article. There is a lot of code and now it is rolled up.



Table code
 <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>
      
      







This example implements a certain minimum for redefinition and exposes methods of interaction with api that are missing in api. Thanks to this, we can integrate the table component in the bowels of the framework with another component of the input field, adding our own filtering functionality.



 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(); } };
      
      





It is conceptually not very correct to implement filtering directly in the event handler, for this we now have a table class and structures of a foundation and data adapter that are closer in meaning, however, our task today is to make sure that it is possible to organize the interaction of components. And we just managed to connect two components that do not have common implementation logic with code in the context of browser execution.





after entering data in the field, the content will be filtered





This example, especially after putting all the javascript logic into separate class files, as we did in the first article of the series , can be a starting point for you to reuse components of angular / material cdk or another toolkit for your own development, redefining the behavior so that “Collective farm” everything from scratch or by integrating new code into the existing infrastructure, ceasing to increase the monolith, because Web components provide the best way to modularly organize your development.



All Articles