Skip to content

Commit

Permalink
feat: add items and renderer to dashboard (#7680)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomivirkki authored Aug 21, 2024
1 parent 3443482 commit f7051b0
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 16 deletions.
81 changes: 81 additions & 0 deletions dev/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,89 @@
<title>Dashboard</title>
<script type="module" src="./common.js"></script>

<style>
vaadin-dashboard-widget {
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 10px;
}

vaadin-dashboard {
--vaadin-dashboard-col-min-width: 300px;
--vaadin-dashboard-col-max-width: 500px;
--vaadin-dashboard-gap: 20px;
--vaadin-dashboard-col-max-count: 3;
}

.kpi-number {
font-size: 80px;
font-weight: bold;
color: #4caf50;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}

.chart {
height: 300px;
background: repeating-linear-gradient(45deg, #e0e0e0, #e0e0e0 10px, #f5f5f5 10px, #f5f5f5 20px);
}
</style>

<script type="module">
import '@vaadin/dashboard';

const dashboard = document.querySelector('vaadin-dashboard');

dashboard.items = [
{
title: 'Total cost',
content: '+203%',
type: 'kpi',
header: '2023-2024',
colspan: 1,
rowspan: 1,
},
{
title: 'Sales',
type: 'chart',
header: '2023-2024',
colspan: 2,
rowspan: 1,
},
{
title: 'Sales closed this month',
content: '54 000€',
type: 'kpi',
colspan: 1,
rowspan: 1,
},
{
title: 'Just some number',
content: '1234',
type: 'kpi',
header: '2014-2024',
colspan: 1,
rowspan: 1,
},
{
title: 'Activity since 2023',
type: 'chart',
colspan: 1,
rowspan: 1,
},
];

dashboard.renderer = (root, _dashboard, { item }) => {
root.innerHTML = `
<vaadin-dashboard-widget widget-title="${item.title}">
<span slot="header">${item.header}</span>
${item.type === 'chart' ? '<div class="chart"></div>' : `<div class="kpi-number">${item.content}</div>`}
</vaadin-dashboard-widget>
`;
};
</script>
</head>

Expand Down
4 changes: 3 additions & 1 deletion packages/dashboard/src/vaadin-dashboard-layout-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ export const DashboardLayoutMixin = (superClass) =>
}
::slotted(*) {
grid-column: span
--_vaadin-dashboard-item-column: span
min(
var(--vaadin-dashboard-item-colspan, 1),
var(--_vaadin-dashboard-effective-col-count, var(--_vaadin-dashboard-col-count))
);
grid-column: var(--_vaadin-dashboard-item-column);
}
`;
}
Expand Down
1 change: 1 addition & 0 deletions packages/dashboard/src/vaadin-dashboard-widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class DashboardWidget extends ControllerMixin(ElementMixin(PolylitMixin(LitEleme
:host {
display: flex;
flex-direction: column;
grid-column: var(--_vaadin-dashboard-item-column);
}
:host([hidden]) {
Expand Down
41 changes: 39 additions & 2 deletions packages/dashboard/src/vaadin-dashboard.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,51 @@
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { DashboardLayoutMixin } from './vaadin-dashboard-layout-mixin.js';

export interface DashboardItem {
/**
* The column span of the item
*/
colspan?: number;
}

export interface DashboardItemModel<TItem> {
item: TItem;
}

export type DashboardRenderer<TItem extends DashboardItem> = (
root: HTMLElement,
owner: Dashboard<TItem>,
model: DashboardItemModel<TItem>,
) => void;

/**
* A responsive, grid-based dashboard layout component
*/
declare class Dashboard extends DashboardLayoutMixin(ElementMixin(HTMLElement)) {}
declare class Dashboard<TItem extends DashboardItem = DashboardItem> extends DashboardLayoutMixin(
ElementMixin(HTMLElement),
) {
/**
* An array containing the items of the dashboard
*/
items: TItem[];

/**
* Custom function for rendering a widget for each dashboard item.
* Placing something else than a widget in the cell is not supported.
* Receives three arguments:
*
* - `root` The container for the widget.
* - `dashboard` The reference to the `<vaadin-dashboard>` element.
* - `model` The object with the properties related with the rendered
* item, contains:
* - `model.item` The item.
*/
renderer: DashboardRenderer<TItem> | null | undefined;
}

declare global {
interface HTMLElementTagNameMap {
'vaadin-dashboard': Dashboard;
'vaadin-dashboard': Dashboard<DashboardItem>;
}
}

Expand Down
75 changes: 72 additions & 3 deletions packages/dashboard/src/vaadin-dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
* license.
*/
import './vaadin-dashboard-widget.js';
import { html, LitElement } from 'lit';
import { html, LitElement, render } from 'lit';
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
import { css, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { DashboardLayoutMixin } from './vaadin-dashboard-layout-mixin.js';

/**
Expand All @@ -22,8 +23,9 @@ import { DashboardLayoutMixin } from './vaadin-dashboard-layout-mixin.js';
* @extends HTMLElement
* @mixes ElementMixin
* @mixes DashboardLayoutMixin
* @mixes ThemableMixin
*/
class Dashboard extends DashboardLayoutMixin(ElementMixin(PolylitMixin(LitElement))) {
class Dashboard extends DashboardLayoutMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement)))) {
static get is() {
return 'vaadin-dashboard';
}
Expand All @@ -32,9 +34,76 @@ class Dashboard extends DashboardLayoutMixin(ElementMixin(PolylitMixin(LitElemen
return 'vaadin-dashboard';
}

static get styles() {
return [
super.styles,
css`
::slotted(vaadin-dashboard-cell) {
display: contents;
}
`,
];
}

static get properties() {
return {
/**
* An array containing the items of the dashboard
* @type {!Array<!DashboardItem> | null | undefined}
*/
items: {
type: Array,
},

/**
* Custom function for rendering a widget for each dashboard item.
* Placing something else than a widget in the cell is not supported.
* Receives three arguments:
*
* - `root` The container for the widget.
* - `dashboard` The reference to the `<vaadin-dashboard>` element.
* - `model` The object with the properties related with the rendered
* item, contains:
* - `model.item` The item.
*
* @type {DashboardRenderer | null | undefined}
*/
renderer: {
type: Function,
},
};
}

static get observers() {
return ['__itemsOrRendererChanged(items, renderer)'];
}

/** @protected */
render() {
return html``;
return html`<slot></slot>`;
}

/** @private */
__itemsOrRendererChanged(items, renderer) {
render(this.__renderItemCells(items || []), this);

this.querySelectorAll('vaadin-dashboard-cell').forEach((cell) => {
if (renderer) {
renderer(cell, this, { item: cell.__item });
} else {
cell.innerHTML = '';
}
});
}

/** @private */
__renderItemCells(items) {
return items.map((item) => {
return html`<vaadin-dashboard-cell
.__item="${item}"
style="--vaadin-dashboard-item-colspan: ${item.colspan};"
></vaadin-dashboard-cell>`;
});
}
}

Expand Down
91 changes: 87 additions & 4 deletions packages/dashboard/test/dashboard.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,81 @@
import { expect } from '@vaadin/chai-plugins';
import { fixtureSync } from '@vaadin/testing-helpers';
import { fixtureSync, nextFrame } from '@vaadin/testing-helpers';
import '../vaadin-dashboard.js';
import type { CustomElementType } from '@vaadin/component-base/src/define.js';
import type { Dashboard } from '../vaadin-dashboard.js';
import type { Dashboard, DashboardItem } from '../vaadin-dashboard.js';
import { getElementFromCell, setGap, setMaximumColumnWidth, setMinimumColumnWidth } from './helpers.js';

type TestDashboardItem = DashboardItem & { id: string };

describe('dashboard', () => {
let dashboard: Dashboard;
let dashboard: Dashboard<TestDashboardItem>;
const columnWidth = 100;

beforeEach(() => {
beforeEach(async () => {
dashboard = fixtureSync('<vaadin-dashboard></vaadin-dashboard>');
dashboard.style.width = `${columnWidth * 100}px`;
setMinimumColumnWidth(dashboard, columnWidth);
setMaximumColumnWidth(dashboard, columnWidth);
setGap(dashboard, 0);

dashboard.items = [{ id: 'Item 0' }, { id: 'Item 1' }];
dashboard.renderer = (root, _, model) => {
root.textContent = '';
const widget = document.createElement('vaadin-dashboard-widget');
widget.widgetTitle = `${model.item.id} title`;
root.appendChild(widget);
};
await nextFrame();
});

it('should render a widget for each item', () => {
const widgets = [getElementFromCell(dashboard, 0, 0), getElementFromCell(dashboard, 0, 1)];
widgets.forEach((widget, index) => {
expect(widget).to.be.ok;
expect(widget?.localName).to.equal('vaadin-dashboard-widget');
expect(widget).to.have.property('widgetTitle', `Item ${index} title`);
});
});

it('should render a new widget', async () => {
dashboard.items = [...dashboard.items, { id: 'Item 2' }];
await nextFrame();

const newWidget = getElementFromCell(dashboard, 0, 2);
expect(newWidget).to.be.ok;
expect(newWidget?.localName).to.equal('vaadin-dashboard-widget');
expect(newWidget).to.have.property('widgetTitle', 'Item 2 title');
});

it('should update the renderer', async () => {
dashboard.renderer = (root, _, model) => {
root.textContent = '';
const widget = document.createElement('vaadin-dashboard-widget');
widget.widgetTitle = `${model.item.id} new title`;
root.appendChild(widget);
};
await nextFrame();

const widgets = [getElementFromCell(dashboard, 0, 0), getElementFromCell(dashboard, 0, 1)];
widgets.forEach((widget, index) => {
expect(widget).to.be.ok;
expect(widget?.localName).to.equal('vaadin-dashboard-widget');
expect(widget).to.have.property('widgetTitle', `Item ${index} new title`);
});
});

it('should clear the items', async () => {
dashboard.items = [];
await nextFrame();

expect(dashboard.querySelectorAll('vaadin-dashboard-widget')).to.be.empty;
});

it('should clear the renderer', async () => {
dashboard.renderer = undefined;
await nextFrame();

expect(dashboard.querySelectorAll('vaadin-dashboard-widget')).to.be.empty;
});

describe('custom element definition', () => {
Expand All @@ -26,4 +93,20 @@ describe('dashboard', () => {
expect((customElements.get(tagName) as CustomElementType).is).to.equal(tagName);
});
});

describe('column span', () => {
it('should span one column by default', () => {
const widgets = [getElementFromCell(dashboard, 0, 0), getElementFromCell(dashboard, 0, 1)];
expect(widgets[0]).to.not.equal(widgets[1]);
});

it('should span multiple columns', async () => {
dashboard.items = [{ colspan: 2, id: 'Item 0' }];
await nextFrame();

const widget = getElementFromCell(dashboard, 0, 0);
expect(widget).to.have.property('widgetTitle', 'Item 0 title');
expect(getElementFromCell(dashboard, 0, 1)).to.equal(widget);
});
});
});
5 changes: 4 additions & 1 deletion packages/dashboard/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ export function getElementFromCell(dashboard: HTMLElement, rowIndex: number, col
const x = left + columnWidths.slice(0, columnIndex).reduce((sum, width) => sum + width, 0);
const y = top + rowHeights.slice(0, rowIndex).reduce((sum, height) => sum + height, 0);

return document.elementFromPoint(x + columnWidths[columnIndex] / 2, y + rowHeights[rowIndex] / 2);
return document
.elementsFromPoint(x + columnWidths[columnIndex] / 2, y + rowHeights[rowIndex] / 2)
.reverse()
.find((element) => dashboard.contains(element) && element !== dashboard)!;
}

/**
Expand Down
Loading

0 comments on commit f7051b0

Please sign in to comment.