Skip to content

Commit 3b4639d

Browse files
Collection rendering performance improvements Part 1: Improve Entity actions render performance (#19605)
* Add null checks for editPath and name in render method The render method now checks for the presence of both editPath and _name before rendering the button, preventing potential errors when these values are missing. * Refactor dropdown open state handling Replaces the public 'open' property with a private field and getter/setter to better control dropdown state. Moves popover open/close logic into the setter, removes the 'updated' lifecycle method, and conditionally renders dropdown content based on the open state. * add opened and closed events * dispatch opened and closed events * Render dropdown content only when open Introduces an _isOpen state to control rendering of the dropdown content in UmbEntityActionsBundleElement. Dropdown content is now only rendered when the dropdown is open, improving performance and preventing unnecessary DOM updates. * Update dropdown.element.ts * create a cache elements * Optimize entity actions observation with IntersectionObserver Adds an IntersectionObserver to only observe entity actions when the element is in the viewport, improving performance. Refactors element creation to use constructors, updates event handling, and ensures cleanup in disconnectedCallback. * only observe once * Update entity-actions-bundle.element.ts * Update dropdown.element.ts * Update entity-actions-bundle.element.ts * split dropdown component * pass compact prop * fix label * Update entity-actions-dropdown.element.ts * Update entity-actions-dropdown.element.ts --------- Co-authored-by: Niels Lyngsø <[email protected]>
1 parent d5159ae commit 3b4639d

File tree

12 files changed

+201
-110
lines changed

12 files changed

+201
-110
lines changed

src/Umbraco.Web.UI.Client/src/packages/core/components/dropdown/dropdown.element.ts

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,28 @@ import type {
55
UUIPopoverContainerElement,
66
} from '@umbraco-cms/backoffice/external/uui';
77
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
8-
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
98
import { css, html, customElement, property, query, when } from '@umbraco-cms/backoffice/external/lit';
109
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
10+
import { UmbClosedEvent, UmbOpenedEvent } from '@umbraco-cms/backoffice/event';
1111

1212
// TODO: maybe move this to UI Library.
1313
@customElement('umb-dropdown')
1414
export class UmbDropdownElement extends UmbLitElement {
15+
#open = false;
16+
1517
@property({ type: Boolean, reflect: true })
16-
open = false;
18+
public get open() {
19+
return this.#open;
20+
}
21+
public set open(value) {
22+
this.#open = value;
23+
24+
if (value === true && this.popoverContainerElement) {
25+
this.openDropdown();
26+
} else {
27+
this.closeDropdown();
28+
}
29+
}
1730

1831
@property()
1932
label?: string;
@@ -36,36 +49,33 @@ export class UmbDropdownElement extends UmbLitElement {
3649
@query('#dropdown-popover')
3750
popoverContainerElement?: UUIPopoverContainerElement;
3851

39-
protected override updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
40-
super.updated(_changedProperties);
41-
if (_changedProperties.has('open') && this.popoverContainerElement) {
42-
if (this.open) {
43-
this.openDropdown();
44-
} else {
45-
this.closeDropdown();
46-
}
47-
}
48-
}
49-
50-
#onToggle(event: ToggleEvent) {
52+
openDropdown() {
5153
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
5254
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
5355
// @ts-ignore
54-
this.open = event.newState === 'open';
56+
this.popoverContainerElement?.showPopover();
57+
this.#open = true;
5558
}
5659

57-
openDropdown() {
60+
closeDropdown() {
5861
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
5962
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
6063
// @ts-ignore
61-
this.popoverContainerElement?.showPopover();
64+
this.popoverContainerElement?.hidePopover();
65+
this.#open = false;
6266
}
6367

64-
closeDropdown() {
68+
#onToggle(event: ToggleEvent) {
6569
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
6670
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
6771
// @ts-ignore
68-
this.popoverContainerElement?.hidePopover();
72+
this.open = event.newState === 'open';
73+
74+
if (this.open) {
75+
this.dispatchEvent(new UmbOpenedEvent());
76+
} else {
77+
this.dispatchEvent(new UmbClosedEvent());
78+
}
6979
}
7080

7181
override render() {
@@ -81,7 +91,7 @@ export class UmbDropdownElement extends UmbLitElement {
8191
<slot name="label"></slot>
8292
${when(
8393
!this.hideExpand,
84-
() => html`<uui-symbol-expand id="symbol-expand" .open=${this.open}></uui-symbol-expand>`,
94+
() => html`<uui-symbol-expand id="symbol-expand" .open=${this.#open}></uui-symbol-expand>`,
8595
)}
8696
</uui-button>
8797
<uui-popover-container id="dropdown-popover" .placement=${this.placement} @toggle=${this.#onToggle}>
@@ -97,6 +107,7 @@ export class UmbDropdownElement extends UmbLitElement {
97107
css`
98108
#dropdown-button {
99109
min-width: max-content;
110+
height: 100%;
100111
}
101112
:host(:not([hide-expand]):not([compact])) #dropdown-button {
102113
--uui-button-padding-right-factor: 2;

src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
11
import { UmbEntityContext } from '../../entity/entity.context.js';
2-
import type { UmbDropdownElement } from '../dropdown/index.js';
32
import type { UmbEntityAction, ManifestEntityActionDefaultKind } from '@umbraco-cms/backoffice/entity-action';
43
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
5-
import {
6-
html,
7-
nothing,
8-
customElement,
9-
property,
10-
state,
11-
ifDefined,
12-
css,
13-
query,
14-
} from '@umbraco-cms/backoffice/external/lit';
4+
import { html, nothing, customElement, property, state, ifDefined, css } from '@umbraco-cms/backoffice/external/lit';
155
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
166
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
177
import { UmbExtensionsManifestInitializer, createExtensionApi } from '@umbraco-cms/backoffice/extension-api';
@@ -39,11 +29,32 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
3929
@state()
4030
private _firstActionHref?: string;
4131

42-
@query('#action-modal')
43-
private _dropdownElement?: UmbDropdownElement;
44-
4532
// TODO: provide the entity context on a higher level, like the root element of this entity, tree-item/workspace/... [NL]
4633
#entityContext = new UmbEntityContext(this);
34+
#inViewport = false;
35+
#observingEntityActions = false;
36+
37+
constructor() {
38+
super();
39+
40+
// Only observe entity actions when the element is in the viewport
41+
const observer = new IntersectionObserver(
42+
(entries) => {
43+
entries.forEach((entry) => {
44+
if (entry.isIntersecting) {
45+
this.#inViewport = true;
46+
this.#observeEntityActions();
47+
}
48+
});
49+
},
50+
{
51+
root: null, // Use the viewport as the root
52+
threshold: 0.1, // Trigger when at least 10% of the element is visible
53+
},
54+
);
55+
56+
observer.observe(this);
57+
}
4758

4859
protected override updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
4960
if (_changedProperties.has('entityType') && _changedProperties.has('unique')) {
@@ -54,6 +65,11 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
5465
}
5566

5667
#observeEntityActions() {
68+
if (!this.entityType) return;
69+
if (this.unique === undefined) return;
70+
if (!this.#inViewport) return; // Only observe if the element is in the viewport
71+
if (this.#observingEntityActions) return;
72+
5773
new UmbExtensionsManifestInitializer(
5874
this,
5975
umbExtensionsRegistry,
@@ -67,6 +83,8 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
6783
},
6884
'umbEntityActionsObserver',
6985
);
86+
87+
this.#observingEntityActions = true;
7088
}
7189

7290
async #createFirstActionApi() {
@@ -90,14 +108,6 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
90108
await this._firstActionApi?.execute().catch(() => {});
91109
}
92110

93-
#onActionExecuted() {
94-
this._dropdownElement?.closeDropdown();
95-
}
96-
97-
#onDropdownClick(event: Event) {
98-
event.stopPropagation();
99-
}
100-
101111
override render() {
102112
if (this._numberOfActions === 0) return nothing;
103113
return html`<uui-action-bar slot="actions">${this.#renderMore()} ${this.#renderFirstAction()} </uui-action-bar>`;
@@ -107,16 +117,9 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
107117
if (this._numberOfActions === 1) return nothing;
108118

109119
return html`
110-
<umb-dropdown id="action-modal" @click=${this.#onDropdownClick} .label=${this.label} compact hide-expand>
111-
<uui-symbol-more slot="label" .label=${this.label}></uui-symbol-more>
112-
<uui-scroll-container>
113-
<umb-entity-action-list
114-
@action-executed=${this.#onActionExecuted}
115-
.entityType=${this.entityType}
116-
.unique=${this.unique}
117-
.label=${this.label}></umb-entity-action-list>
118-
</uui-scroll-container>
119-
</umb-dropdown>
120+
<umb-entity-actions-dropdown .label=${this.label} compact>
121+
<uui-symbol-more slot="label"></uui-symbol-more>
122+
</umb-entity-actions-dropdown>
120123
`;
121124
}
122125

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { UmbDropdownElement } from '../../../components/dropdown/index.js';
2+
import { UmbEntityActionListElement } from '../../entity-action-list.element.js';
3+
import { html, customElement, property, css, query } from '@umbraco-cms/backoffice/external/lit';
4+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
5+
import { UUIScrollContainerElement } from '@umbraco-cms/backoffice/external/uui';
6+
import { UMB_ENTITY_CONTEXT, type UmbEntityModel } from '@umbraco-cms/backoffice/entity';
7+
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
8+
9+
@customElement('umb-entity-actions-dropdown')
10+
export class UmbEntityActionsDropdownElement extends UmbLitElement {
11+
@property({ type: Boolean })
12+
compact = false;
13+
14+
@property({ type: String })
15+
public label?: string;
16+
17+
@query('#action-modal')
18+
private _dropdownElement?: UmbDropdownElement;
19+
20+
#scrollContainerElement?: UUIScrollContainerElement;
21+
#entityActionListElement?: UmbEntityActionListElement;
22+
#entityType?: UmbEntityModel['entityType'];
23+
#unique?: UmbEntityModel['unique'];
24+
25+
constructor() {
26+
super();
27+
this.consumeContext(UMB_ENTITY_CONTEXT, (context) => {
28+
if (!context) return;
29+
30+
this.observe(observeMultiple([context.entityType, context.unique]), ([entityType, unique]) => {
31+
this.#entityType = entityType;
32+
this.#unique = unique;
33+
34+
if (this.#entityActionListElement) {
35+
this.#entityActionListElement.entityType = entityType;
36+
this.#entityActionListElement.unique = unique;
37+
}
38+
});
39+
});
40+
}
41+
42+
#onActionExecuted() {
43+
this._dropdownElement?.closeDropdown();
44+
}
45+
46+
#onDropdownClick(event: Event) {
47+
event.stopPropagation();
48+
}
49+
50+
#onDropdownOpened() {
51+
if (this.#scrollContainerElement) {
52+
return; // Already created
53+
}
54+
55+
// First create dropdown content when the dropdown is opened.
56+
// Programmatically create the elements so they are cached if the dropdown is opened again
57+
this.#scrollContainerElement = new UUIScrollContainerElement();
58+
this.#entityActionListElement = new UmbEntityActionListElement();
59+
this.#entityActionListElement.addEventListener('action-executed', this.#onActionExecuted);
60+
this.#entityActionListElement.entityType = this.#entityType;
61+
this.#entityActionListElement.unique = this.#unique;
62+
this.#entityActionListElement.setAttribute('label', this.label ?? '');
63+
this.#scrollContainerElement.appendChild(this.#entityActionListElement);
64+
this._dropdownElement?.appendChild(this.#scrollContainerElement);
65+
}
66+
67+
override render() {
68+
return html`<umb-dropdown
69+
id="action-modal"
70+
@click=${this.#onDropdownClick}
71+
@opened=${this.#onDropdownOpened}
72+
.label=${this.label}
73+
?compact=${this.compact}
74+
hide-expand>
75+
<slot name="label" slot="label"></slot>
76+
<slot></slot>
77+
</umb-dropdown>`;
78+
}
79+
80+
static override styles = [
81+
css`
82+
uui-scroll-container {
83+
max-height: 700px;
84+
}
85+
`,
86+
];
87+
}
88+
89+
declare global {
90+
interface HTMLElementTagNameMap {
91+
'umb-entity-actions-dropdown': UmbEntityActionsDropdownElement;
92+
}
93+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
import './entity-actions-dropdown/entity-actions-dropdown.element.js';
12
import './entity-actions-table-column-view/entity-actions-table-column-view.element.js';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export class UmbClosedEvent extends Event {
2+
public static readonly TYPE = 'closed';
3+
4+
public constructor() {
5+
// mimics the native toggle event
6+
super(UmbClosedEvent.TYPE, { bubbles: false, composed: false, cancelable: false });
7+
}
8+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
export * from './action-executed.event.js';
22
export * from './change.event.js';
3+
export * from './closed.event.js';
34
export * from './delete.event.js';
45
export * from './deselected.event.js';
56
export * from './input.event.js';
7+
export * from './opened.event.js';
68
export * from './progress.event.js';
79
export * from './selected.event.js';
810
export * from './selection-change.event.js';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export class UmbOpenedEvent extends Event {
2+
public static readonly TYPE = 'opened';
3+
4+
public constructor() {
5+
// mimics the native toggle event
6+
super(UmbOpenedEvent.TYPE, { bubbles: false, composed: false, cancelable: false });
7+
}
8+
}

0 commit comments

Comments
 (0)