Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: extract crud logic into reusable mixins #8244

Merged
merged 2 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/crud/src/vaadin-crud-grid-mixin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Constructor } from '@open-wc/dedupe-mixin';
web-padawan marked this conversation as resolved.
Show resolved Hide resolved
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;
}
266 changes: 266 additions & 0 deletions packages/crud/src/vaadin-crud-grid-mixin.js
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;
}
};
22 changes: 2 additions & 20 deletions packages/crud/src/vaadin-crud-grid.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,12 @@
* license.
*/
import { Grid } from '@vaadin/grid/src/vaadin-grid.js';
import { IncludedMixin } from './vaadin-crud-include-mixin.js';
import { CrudGridMixin } from './vaadin-crud-grid-mixin.js';

/**
* An element used internally by `<vaadin-crud>`. Not intended to be used separately.
*/
declare class CrudGrid extends IncludedMixin(Grid) {
/**
* 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;
}
declare class CrudGrid extends CrudGridMixin(Grid) {}

declare global {
interface HTMLElementTagNameMap {
Expand Down
Loading