@furo/ui5
Version:
SAP UI5 Web Components data bindings for furo-web
449 lines (410 loc) • 11.4 kB
JavaScript
import { LitElement, html, css } from 'lit';
import { Env } from '@furo/framework';
import { FBP } from '@furo/fbp/src/fbp.js';
import '@furo/fbp/src/flow-repeat.js';
// eslint-disable-next-line import/no-extraneous-dependencies
import '@furo/data/src/furo-type-renderer.js';
// eslint-disable-next-line import/no-extraneous-dependencies
import './furo-ui5-typerenderer-labeled.js';
import '@ui5/webcomponents/dist/Table.js';
import '@ui5/webcomponents/dist/TableColumn.js';
import '@ui5/webcomponents/dist/TableCell.js';
import './subcomponents/furo-ui5-table-row.js';
import { html as statichtml, literal } from 'lit/static-html.js';
/**
* `furo-ui5-table` display entities in a ui5-table
*
* ```html
* <furo-ui5-table
* fn-bind-data="--data(*.entities)"
* >
* <!-- The column label is evaluated from the specs -->
* <ui5-table-column
* slot="columns"
* field="*.data.fieldname"
* ></ui5-table-column>
*
* <ui5-table-column
* slot="columns"
* field="*.data.display_name"
* ><span>Custom Title</span></ui5-table-column>
*
* </furo-ui5-table>
* ```
*
* ## Attributes which are taken from `ui5-table-column`
*
* **field**
* Define the field you want to bind. `*` is the root of the repeated field.
*
* **context**
* Set a context for the type renderer. The default value is `cell`.
*
* **renderer**
* Set a specific renderer component for the column. If not set, the renderer is evaluated from the type of the bound field.
*
*
*
* @fires {entity} arrow-down-on-last-row - Fired when the ArrowDown is pressed on the last row. The event detail is the original entity of the row
* @fires {entity} tablerow-selected - Fired when the row is selected. The event detail is the original entity of the row.
* @fires {entity} arrow-up-on-first-row - Fired when the ArrowUp is pressed on the first row. The event detail is the original entity of the row
* @fires {HTMLElement} data-loaded - Fired when the data is loaded into data table. The event detail contains the data table self.
* @fires {Array with the selected items} rows-selected - Fired when the row selection in MultiSelect mode was changed
*
*
* @element
* @summary Display repeated fields in a table
*/
export class FuroUi5Table extends FBP(LitElement) {
constructor() {
super();
/**
*
* @private
*/
this.cols = [];
/**
*
* @private
*/
this._specs = Env.api.specs;
/**
*
* @private
*/
this.data = [];
this.mode = 'None';
}
/**
*
* @private
*/
_FBPReady() {
super._FBPReady();
/**
* Listen to selection change from the table and build up a list with data items.
*/
this.shadowRoot
.querySelector('ui5-table')
.addEventListener('selectionChange', e => {
const affectedItems = [];
e.detail.selectedRows.forEach(r => {
if (r._data) {
affectedItems.push(r._data._value);
}
});
/**
* @event rows-selected
* Fired when the row selection in MultiSelect mode was changed
* detail payload: Array with the selected items
*/
const customEvent = new Event('rows-selected', {
composed: true,
bubbles: true,
});
customEvent.detail = affectedItems;
this.dispatchEvent(customEvent);
});
}
/**
* Bind a repeated data node.
* @param data
*/
bindData(data) {
if (!data._isRepeater) {
// eslint-disable-next-line no-console
console.warn(
'Invalid fieldNode in bindData. please bind a repeated field.'
);
return;
}
// spec metas can change
this._fields = this._specs[data._spec.type].fields;
// protection against multiple calls of bindData
if (this.__fieldNode && this.__fieldNode.removeEventListener) {
// eslint-disable-next-line no-console
console.warn('BindData can only be called once.');
}
this._init();
// add the main event listeners
data.addEventListener('repeated-fields-all-removed', e => {
this.data = e.detail;
this._FBPTriggerWire('--data', this.data);
if (this.noDataText) {
this._showNoData = true;
}
this.requestUpdate();
});
/**
* new data arrived from CollectionNode
*/
data.addEventListener('this-repeated-field-changed', e => {
this.data = e.detail.repeats;
this._FBPTriggerWire('--data', this.data);
if (this.data.length > 0) {
this._showNoData = false;
} else if (this.noDataText) {
this._showNoData = true;
}
this.requestUpdate();
});
this.__fieldNode = data;
this.dispatchEvent(
new CustomEvent('data-loaded', {
detail: this,
bubbles: true,
composed: true,
})
);
}
/**
* Focuses the header of the table
*/
focus() {
const table = this.shadowRoot.querySelector('ui5-table');
if (table && table.shadowRoot) {
const header = table.shadowRoot.querySelector('tr');
// fix error when navigate without data
header.addEventListener('keydown', e => {
const key = e.key || e.keyCode;
if ((key === 'ArrowDown' || key === 40) && this.data.length === 0) {
e.stopPropagation();
}
});
header.click();
}
}
/**
* Focuses the last row.
*/
focusLast() {
this._FBPTriggerWire('--triggerLast');
}
/**
* Focuses the first row.
*/
focusFirst() {
this._FBPTriggerWire('--triggerFirst');
}
/**
* init data table
* @private
*/
_init() {
this._ctx = [];
this._wires = [];
this._renderer = [];
const fieldPaths = [];
this.querySelectorAll('ui5-table-column').forEach(col => {
const fieldPath = col.getAttribute('field');
fieldPaths.push(fieldPath);
this._ctx.push(col.getAttribute('context') || 'cell');
this._renderer.push(col.getAttribute('renderer') || 'furo-type-renderer');
// if no title was set, use the title from the field spec
if (col.childElementCount === 0) {
const fieldNode = this._getSpecFieldFromPath(this._fields, fieldPath);
const span = document.createElement('span');
span.innerText = fieldNode?.meta?.label || fieldPath;
col.appendChild(span);
}
this._wires.push(`--init(${fieldPath})`);
// append to table
this.shadowRoot.querySelector('ui5-table').appendChild(col);
});
this._initRepeatTemplate(fieldPaths);
this._showNoData = !!this.noDataText;
this.requestUpdate();
}
_initRepeatTemplate(fieldPaths) {
this._rowRepeatTemplate = statichtml`<template>
<furo-ui5-table-row fn-set-data='--init(*)' fn-focus='--trigger'>
${this._cellMap(fieldPaths)}
</furo-ui5-table-row>
</template>`;
}
/**
* ui5 data table cell template
* @param fields
* @private
* @returns {TemplateResult|TemplateResult}
*/
_cellMap(fieldPaths) {
const items = [];
fieldPaths.forEach((fieldPath, index) => {
items.push(`<ui5-table-cell><${this._renderer[index]} `);
items.push(`fn-bind-data="${this._wires[index]}" `);
items.push(`context="${this._ctx[index]}"`);
items.push(`></${this._renderer[index]}></ui5-table-cell>`);
});
return literal([items.join('')]);
}
/**
* resolves a field specification with the given path
* @param field
* @param path
* @returns {*}
* @private
*/
// eslint-disable-next-line consistent-return
_getSpecFieldFromPath(field, path) {
if (path.startsWith('*')) {
if (path.length === 1) {
return field;
}
// cut of *.
// eslint-disable-next-line no-param-reassign
path = path.substr(2, path.length - 1);
}
if (field) {
const prop = field;
const parts = this._split(path);
if (parts.length > 1) {
if (field.fields && field.fields[parts[0]]) {
return this._getSpecFieldFromPath(
field.fields[parts[0]],
parts.slice(1).join('.')
);
}
if (!field[parts[0]]) {
return this._getSpecFieldFromPath(
this._specs[field.type],
parts.join('.')
);
}
return this._getSpecFieldFromPath(
field[parts[0]],
parts.slice(1).join('.')
);
}
const part = parts[0];
if (prop.fields && prop.fields[part] !== undefined) {
return prop.fields[part];
}
if (field[path]) {
return field[path];
}
return this._getSpecFieldFromPath(this._specs[field.type], part);
}
// eslint-disable-next-line no-console
console.warn(
`Invalid subfield '${path}' in the field-path. please set a correct field-path in cloumn.`
);
}
/**
* setBusy Sets the busy state
* @public
*/
setBusy() {
this.busy = true;
}
/**
* unsetBusy Unsets the busy state
* @public
*/
unsetBusy() {
this.busy = false;
}
/**
* @private
* @returns {CSSResult}
*/
static get styles() {
return css`
:host {
display: block;
font-variant-numeric: lining-nums tabular-nums;
}
:host([hidden]) {
display: none;
}
.no-data {
height: 3rem;
text-align: center;
line-height: 3rem;
}
`;
}
/**
*@private
*/
static get properties() {
return {
/**
* Defines the mode of the component.
*
* Available options are:
* - MultiSelect
* - SingleSelect
* - None
*
* @type String
*/
mode: {
type: String,
},
/**
* Defines the text that will be displayed when there is no data.
* string
*
* @type String
*/
noDataText: {
type: String,
attribute: 'no-data-text',
},
/**
* Identity path of a single item.
* Use this if you have a field which identifies the item. Bind the value, not the fieldnode => `id._value`.
* @type {*string}
*/
identityPath: {
type: String,
attribute: 'identity-path',
},
/**
* Determines whether the column headers remain fixed at the top of the page during vertical scrolling as long as the Web Component is in the viewport.
*
* @type Boolean
*/
stickyColumnHeader: {
type: Boolean,
attribute: 'sticky-column-header',
},
/**
* Busy state
*
* @type Boolean
*/
busy: {
type: Boolean,
},
};
}
/**
* @private
* @returns {TemplateResult}
*/
render() {
return html`
<ui5-table
?sticky-column-header="${this.stickyColumnHeader}"
mode="${this.mode}"
?busy="${this.busy}"
>
<flow-repeat
fn-inject-items="--data"
fn-trigger-first="--triggerFirst"
fn-trigger-last="--triggerLast"
identity-path="${this.identityPath}"
>
${this._rowRepeatTemplate}
</flow-repeat>
</ui5-table>
<slot></slot>
${this._showNoData
? html` <div class="no-data">${this.noDataText}</div> `
: html``}
`;
}
}
customElements.define('furo-ui5-table', FuroUi5Table);