-
Notifications
You must be signed in to change notification settings - Fork 83
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: extract crud logic into reusable mixins (#8244)
- Loading branch information
1 parent
c63be0f
commit d089b88
Showing
9 changed files
with
1,644 additions
and
1,554 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
/** | ||
* @license | ||
* Copyright (c) 2000 - 2024 Vaadin Ltd. | ||
* | ||
* This program is available under Vaadin Commercial License and Service Terms. | ||
* | ||
* | ||
* See https://vaadin.com/commercial-license-and-service-terms for the full | ||
* license. | ||
*/ | ||
import type { Constructor } from '@open-wc/dedupe-mixin'; | ||
import type { IncludedMixinClass } from './vaadin-crud-include-mixin.js'; | ||
|
||
/** | ||
* A mixin providing common crud grid functionality. | ||
*/ | ||
export declare function CrudGridMixin<T extends Constructor<HTMLElement>>( | ||
base: T, | ||
): Constructor<CrudGridMixinClass> & Constructor<IncludedMixinClass> & T; | ||
|
||
export declare class CrudGridMixinClass { | ||
/** | ||
* Disable filtering in the generated columns. | ||
* @attr {boolean} no-filter | ||
*/ | ||
noFilter: boolean | null | undefined; | ||
|
||
/** | ||
* Disable sorting in the generated columns. | ||
* @attr {boolean} no-sort | ||
*/ | ||
noSort: boolean | null | undefined; | ||
|
||
/** | ||
* Do not add headers to columns. | ||
* @attr {boolean} no-head | ||
*/ | ||
noHead: boolean | null | undefined; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
/** | ||
* @license | ||
* Copyright (c) 2000 - 2024 Vaadin Ltd. | ||
* | ||
* This program is available under Vaadin Commercial License and Service Terms. | ||
* | ||
* | ||
* See https://vaadin.com/commercial-license-and-service-terms for the full | ||
* license. | ||
*/ | ||
import { capitalize, getProperty } from './vaadin-crud-helpers.js'; | ||
import { IncludedMixin } from './vaadin-crud-include-mixin.js'; | ||
|
||
/** | ||
* A mixin providing common crud-grid functionality. | ||
* | ||
* @polymerMixin | ||
* @mixes IncludedMixin | ||
*/ | ||
export const CrudGridMixin = (superClass) => | ||
class extends IncludedMixin(superClass) { | ||
static get properties() { | ||
return { | ||
/** | ||
* Disable filtering in the generated columns. | ||
* @attr {boolean} no-filter | ||
*/ | ||
noFilter: Boolean, | ||
|
||
/** | ||
* Disable sorting in the generated columns. | ||
* @attr {boolean} no-sort | ||
*/ | ||
noSort: Boolean, | ||
|
||
/** | ||
* Do not add headers to columns. | ||
* @attr {boolean} no-head | ||
*/ | ||
noHead: Boolean, | ||
|
||
/** @private */ | ||
__hideEditColumn: Boolean, | ||
}; | ||
} | ||
|
||
static get observers() { | ||
return ['__onItemsChange(items)', '__onHideEditColumnChange(hideEditColumn)']; | ||
} | ||
|
||
/** @private */ | ||
__onItemsChange(items) { | ||
if ((!this.dataProvider || this.dataProvider === this._arrayDataProvider) && !this.include && items && items[0]) { | ||
this._configure(items[0]); | ||
} | ||
} | ||
|
||
/** @private */ | ||
__onHideEditColumnChange() { | ||
if (this.firstChild) { | ||
this.__toggleEditColumn(); | ||
} | ||
} | ||
|
||
/** @private */ | ||
__toggleEditColumn() { | ||
let editColumn = this.querySelector('vaadin-crud-edit-column'); | ||
if (this.hideEditColumn) { | ||
if (editColumn) { | ||
this.removeChild(editColumn); | ||
} | ||
} else if (!editColumn) { | ||
editColumn = document.createElement('vaadin-crud-edit-column'); | ||
editColumn.frozenToEnd = true; | ||
this.appendChild(editColumn); | ||
} | ||
} | ||
|
||
/** @private */ | ||
__dataProviderWrapper(params, callback) { | ||
this.__dataProvider(params, (items, size) => { | ||
if (this.innerHTML === '' && !this.include && items[0]) { | ||
this._configure(items[0]); | ||
} | ||
callback(items, size); | ||
}); | ||
} | ||
|
||
/** | ||
* @override | ||
* @private | ||
*/ | ||
_dataProviderChanged(dataProvider, oldDataProvider) { | ||
if (this._arrayDataProvider === dataProvider) { | ||
super._dataProviderChanged(dataProvider, oldDataProvider); | ||
} else if (this.__dataProviderWrapper !== dataProvider) { | ||
this.innerHTML = ''; | ||
this.__dataProvider = dataProvider; | ||
this.dataProvider = this.__dataProviderWrapper; | ||
super._dataProviderChanged(this.__dataProviderWrapper, oldDataProvider); | ||
} | ||
} | ||
|
||
/** | ||
* Auto-generate grid columns based on the JSON structure of the object provided. | ||
* | ||
* Method will be executed when items or dataProvider is assigned. | ||
* @private | ||
*/ | ||
_configure(item) { | ||
this.innerHTML = ''; | ||
this.__createColumns(this, item, undefined, this.__getPropertyDepth(item)); | ||
this.__toggleEditColumn(); | ||
} | ||
|
||
/** | ||
* Return the deepest property depth of the object | ||
* @private | ||
*/ | ||
__getPropertyDepth(object) { | ||
if (!object || typeof object !== 'object') { | ||
return 0; | ||
} | ||
|
||
return Object.keys(object).reduce((deepest, prop) => { | ||
if (this.exclude && this.exclude.test(prop)) { | ||
return deepest; | ||
} | ||
return Math.max(deepest, 1 + this.__getPropertyDepth(object[prop])); | ||
}, 0); | ||
} | ||
|
||
/** | ||
* Parse the camelCase column names into sentence case headers. | ||
* @param {string} path | ||
* @return {string} | ||
* @protected | ||
*/ | ||
_generateHeader(path) { | ||
return path | ||
.substr(path.lastIndexOf('.') + 1) | ||
.replace(/([A-Z])/gu, '-$1') | ||
.toLowerCase() | ||
.replace(/-/gu, ' ') | ||
.replace(/^./u, (match) => match.toUpperCase()); | ||
} | ||
|
||
/** @private */ | ||
__createColumn(parent, path) { | ||
let col; | ||
if (!this.noFilter && !this.noSort && !parent.__sortColumnGroup) { | ||
// This crud-grid has both a sorter and a filter, but neither has yet been | ||
// created => col should become the sorter group column | ||
col = this.__createGroup(parent); | ||
col.__sortColumnGroup = true; | ||
// Create the filter column under this sorter group column | ||
this.__createColumn(col, path); | ||
} else { | ||
// In all other cases, col should be a regular column with a renderer | ||
col = document.createElement('vaadin-grid-column'); | ||
parent.appendChild(col); | ||
col.renderer = (root, _column, model) => { | ||
root.textContent = path ? getProperty(path, model.item) : model.item; | ||
}; | ||
} | ||
|
||
if (!this.noHead && path) { | ||
// Create a header renderer for the column (or column group) | ||
col.headerRenderer = (root) => { | ||
if (root.firstElementChild) { | ||
return; | ||
} | ||
|
||
const label = this._generateHeader(path); | ||
|
||
if (col.__sortColumnGroup || (this.noFilter && !this.noSort)) { | ||
// The column is either the sorter group column or the root level | ||
// sort column (in case a filter isn't used at all) => add the sort indicator | ||
const sorter = document.createElement('vaadin-grid-sorter'); | ||
sorter.setAttribute('path', path); | ||
// TODO: Localize aria labels | ||
sorter.setAttribute('aria-label', `Sort by ${label}`); | ||
sorter.textContent = label; | ||
root.appendChild(sorter); | ||
} else if (!this.noFilter) { | ||
// Filtering is enabled in this crud-grid, create the filter element | ||
const filter = document.createElement('vaadin-grid-filter'); | ||
filter.setAttribute('path', path); | ||
// TODO: Localize aria labels | ||
filter.setAttribute('aria-label', `Filter by ${label}`); | ||
filter.style.display = 'flex'; | ||
|
||
const textField = window.document.createElement('vaadin-text-field'); | ||
textField.setAttribute('theme', 'small'); | ||
textField.setAttribute('focus-target', true); | ||
textField.style.width = '100%'; | ||
if (this.noSort) { | ||
textField.placeholder = label; | ||
} | ||
textField.addEventListener('value-changed', (event) => { | ||
filter.value = event.detail.value; | ||
}); | ||
|
||
filter.appendChild(textField); | ||
root.appendChild(filter); | ||
} else if (this.noSort && this.noFilter) { | ||
// Neither sorter nor filter are enabled, just add the label | ||
root.textContent = label; | ||
} | ||
}; | ||
} | ||
} | ||
|
||
/** | ||
* Creates the column structure for the (sub)object. | ||
* | ||
* @param {HTMLElement} parent May be the crud-grid or a column group. | ||
* @param {Object} object The object to create the sub-columns for. | ||
* @param {string} path The property path from the root item to the object. | ||
* @param {number} depth The depth of the object in the object hierarchy. | ||
* @private | ||
*/ | ||
__createColumns(parent, object, path, depth) { | ||
if (object && typeof object === 'object') { | ||
// Iterate over the object properties | ||
Object.keys(object).forEach((prop) => { | ||
if (!this.include && this.exclude && this.exclude.test(prop)) { | ||
return; | ||
} | ||
// Sub-object of the current object | ||
const subObject = object[prop]; | ||
// Full path to the sub-object | ||
const subObjectPath = path ? `${path}.${prop}` : prop; | ||
|
||
// The column element for the sub-object | ||
let subObjectColumn = parent; | ||
if (!this.noHead && depth > 1) { | ||
const isSubObject = subObject && typeof subObject === 'object'; | ||
// If the sub-object is an actual object, create a column group with the property | ||
// name as the header text, otherwise create a group without a header | ||
subObjectColumn = this.__createGroup(parent, isSubObject ? prop : undefined); | ||
} | ||
|
||
// Run recursively for the sub-object level | ||
this.__createColumns(subObjectColumn, subObject, subObjectPath, depth - 1); | ||
}); | ||
} else if (depth > 1) { | ||
// The object has been fully traversed, but empty wrapping column | ||
// groups are still needed to complete the full object depth | ||
this.__createColumns(this.__createGroup(parent), undefined, path, depth - 1); | ||
} else { | ||
// The column group depth is complete, create the actual leaf column | ||
this.__createColumn(parent, path); | ||
} | ||
} | ||
|
||
/** @private */ | ||
__createGroup(parent, header) { | ||
const grp = document.createElement('vaadin-grid-column-group'); | ||
if (header) { | ||
grp.header = capitalize(header); | ||
} | ||
parent.appendChild(grp); | ||
return grp; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.