slickgrid-react 5.12.1 → 5.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +48 -7
  2. package/dist/cjs/components/slickgrid-react.js +18 -10
  3. package/dist/cjs/components/slickgrid-react.js.map +1 -1
  4. package/dist/cjs/extensions/slickRowDetailView.js +122 -95
  5. package/dist/cjs/extensions/slickRowDetailView.js.map +1 -1
  6. package/dist/cjs/global-grid-options.js +0 -1
  7. package/dist/cjs/global-grid-options.js.map +1 -1
  8. package/dist/cjs/services/reactUtils.js +9 -3
  9. package/dist/cjs/services/reactUtils.js.map +1 -1
  10. package/dist/esm/components/slickgrid-react.js +18 -10
  11. package/dist/esm/components/slickgrid-react.js.map +1 -1
  12. package/dist/esm/extensions/slickRowDetailView.js +123 -96
  13. package/dist/esm/extensions/slickRowDetailView.js.map +1 -1
  14. package/dist/esm/global-grid-options.js +0 -1
  15. package/dist/esm/global-grid-options.js.map +1 -1
  16. package/dist/esm/services/reactUtils.js +8 -3
  17. package/dist/esm/services/reactUtils.js.map +1 -1
  18. package/dist/types/components/slickgrid-react.d.ts.map +1 -1
  19. package/dist/types/components/slickgridReactProps.d.ts +8 -0
  20. package/dist/types/components/slickgridReactProps.d.ts.map +1 -1
  21. package/dist/types/extensions/slickRowDetailView.d.ts +3 -0
  22. package/dist/types/extensions/slickRowDetailView.d.ts.map +1 -1
  23. package/dist/types/global-grid-options.d.ts.map +1 -1
  24. package/dist/types/services/reactUtils.d.ts +4 -0
  25. package/dist/types/services/reactUtils.d.ts.map +1 -1
  26. package/package.json +39 -42
  27. package/src/slickgrid-react/components/slickgrid-react.tsx +19 -12
  28. package/src/slickgrid-react/components/slickgridReactProps.ts +2 -0
  29. package/src/slickgrid-react/extensions/slickRowDetailView.ts +129 -99
  30. package/src/slickgrid-react/global-grid-options.ts +0 -1
  31. package/src/slickgrid-react/services/reactUtils.ts +11 -3
@@ -14,7 +14,7 @@ import { SlickRowDetailView as UniversalSlickRowDetailView } from '@slickgrid-un
14
14
  import type { Root } from 'react-dom/client';
15
15
 
16
16
  import type { GridOption, RowDetailView, ViewModelBindableInputData } from '../models/index';
17
- import { loadReactComponentDynamically } from '../services/reactUtils';
17
+ import { createReactComponentDynamically } from '../services/reactUtils';
18
18
 
19
19
  const ROW_DETAIL_CONTAINER_PREFIX = 'container_';
20
20
  const PRELOAD_CONTAINER_PREFIX = 'container_loading';
@@ -23,12 +23,14 @@ export interface CreatedView {
23
23
  id: string | number;
24
24
  dataContext: any;
25
25
  root: Root | null;
26
+ rendered?: boolean;
26
27
  }
27
28
  // interface SRDV extends React.Component<Props, State>, UniversalSlickRowDetailView {}s
28
29
 
29
30
  export class SlickRowDetailView extends UniversalSlickRowDetailView {
30
31
  protected _component?: any;
31
32
  protected _preloadComponent?: any;
33
+ protected _preloadRoot?: Root;
32
34
  protected _views: CreatedView[] = [];
33
35
  protected _subscriptions: EventSubscription[] = [];
34
36
  protected _userProcessFn?: (item: any) => Promise<any>;
@@ -73,10 +75,12 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView {
73
75
 
74
76
  /** Dispose of all the opened Row Detail Panels Components */
75
77
  disposeAllViewComponents() {
76
- if (Array.isArray(this._views)) {
77
- this._views.forEach((view) => this.disposeViewComponent(view));
78
- }
79
- this._views = [];
78
+ do {
79
+ const view = this._views.pop();
80
+ if (view) {
81
+ this.disposeView(view);
82
+ }
83
+ } while (this._views.length > 0);
80
84
  }
81
85
 
82
86
  /** Get the instance of the SlickGrid addon (control or plugin). */
@@ -114,9 +118,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView {
114
118
  }
115
119
  if (!this.gridOptions.rowDetailView.postTemplate) {
116
120
  this._component = this.gridOptions?.rowDetailView?.viewComponent;
117
- this.addonOptions.postTemplate = (itemDetail: any) => {
118
- return this._grid.sanitizeHtmlString(`<div class="${ROW_DETAIL_CONTAINER_PREFIX}${itemDetail[this.datasetIdPropName]}"></div>`) as string
119
- };
121
+ this.addonOptions.postTemplate = (itemDetail: any) => this._grid.sanitizeHtmlString(`<div class="${ROW_DETAIL_CONTAINER_PREFIX}${itemDetail[this.datasetIdPropName]}"></div>`) as string;
120
122
  }
121
123
 
122
124
  if (this._grid && this.gridOptions) {
@@ -132,68 +134,65 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView {
132
134
  this.rowDetailViewOptions.onExtensionRegistered(this);
133
135
  }
134
136
 
135
- if (this.onAsyncResponse) {
136
- this._eventHandler.subscribe(this.onAsyncResponse, (event, args) => {
137
- if (typeof this.rowDetailViewOptions?.onAsyncResponse === 'function') {
138
- this.rowDetailViewOptions.onAsyncResponse(event, args);
139
- }
140
- });
141
- }
142
-
143
- if (this.onAsyncEndUpdate) {
144
- this._eventHandler.subscribe(this.onAsyncEndUpdate, async (event, args) => {
145
- // triggers after backend called "onAsyncResponse.notify()"
146
- await this.renderViewModel(args?.item);
147
-
148
- if (typeof this.rowDetailViewOptions?.onAsyncEndUpdate === 'function') {
149
- this.rowDetailViewOptions.onAsyncEndUpdate(event, args);
150
- }
151
- });
152
- }
153
-
154
- if (this.onAfterRowDetailToggle) {
155
- this._eventHandler.subscribe(this.onAfterRowDetailToggle, async (event, args) => {
156
- // display preload template & re-render all the other Detail Views after toggling
157
- // the preload View will eventually go away once the data gets loaded after the "onAsyncEndUpdate" event
158
- await this.renderPreloadView(args.item);
159
- this.renderAllViewModels();
160
-
161
- if (typeof this.rowDetailViewOptions?.onAfterRowDetailToggle === 'function') {
162
- this.rowDetailViewOptions.onAfterRowDetailToggle(event, args);
163
- }
164
- });
165
- }
166
-
167
- if (this.onBeforeRowDetailToggle) {
168
- this._eventHandler.subscribe(this.onBeforeRowDetailToggle, (event, args) => {
169
- // before toggling row detail, we need to create View Component if it doesn't exist
170
- this.handleOnBeforeRowDetailToggle(event, args);
171
-
172
- if (typeof this.rowDetailViewOptions?.onBeforeRowDetailToggle === 'function') {
173
- return this.rowDetailViewOptions.onBeforeRowDetailToggle(event, args);
174
- }
175
- return true;
176
- });
177
- }
178
-
179
- if (this.onRowBackToViewportRange) {
180
- this._eventHandler.subscribe(this.onRowBackToViewportRange, async (event, args) => {
181
- // when row is back to viewport range, we will re-render the View Component(s)
182
- await this.handleOnRowBackToViewportRange(event, args);
183
-
184
- if (typeof this.rowDetailViewOptions?.onRowBackToViewportRange === 'function') {
185
- this.rowDetailViewOptions.onRowBackToViewportRange(event, args);
186
- }
187
- });
188
- }
189
-
190
- if (this.onRowOutOfViewportRange) {
191
- this._eventHandler.subscribe(this.onRowOutOfViewportRange, (event, args) => {
192
- if (typeof this.rowDetailViewOptions?.onRowOutOfViewportRange === 'function') {
193
- this.rowDetailViewOptions.onRowOutOfViewportRange(event, args);
194
- }
195
- });
196
- }
137
+ this._eventHandler.subscribe(this.onAsyncResponse, (event, args) => {
138
+ if (typeof this.rowDetailViewOptions?.onAsyncResponse === 'function') {
139
+ this.rowDetailViewOptions.onAsyncResponse(event, args);
140
+ }
141
+ });
142
+
143
+ this._eventHandler.subscribe(this.onAsyncEndUpdate, async (event, args) => {
144
+ // dispose preload if exists
145
+ this._preloadRoot?.unmount();
146
+
147
+ // triggers after backend called "onAsyncResponse.notify()"
148
+ await this.renderViewModel(args?.item);
149
+
150
+ if (typeof this.rowDetailViewOptions?.onAsyncEndUpdate === 'function') {
151
+ this.rowDetailViewOptions.onAsyncEndUpdate(event, args);
152
+ }
153
+ });
154
+
155
+ this._eventHandler.subscribe(this.onAfterRowDetailToggle, async (event, args) => {
156
+ // display preload template & re-render all the other Detail Views after toggling
157
+ // the preload View will eventually go away once the data gets loaded after the "onAsyncEndUpdate" event
158
+ await this.renderPreloadView(args.item);
159
+
160
+ if (typeof this.rowDetailViewOptions?.onAfterRowDetailToggle === 'function') {
161
+ this.rowDetailViewOptions.onAfterRowDetailToggle(event, args);
162
+ }
163
+ });
164
+
165
+ this._eventHandler.subscribe(this.onBeforeRowDetailToggle, (event, args) => {
166
+ // before toggling row detail, we need to create View Component if it doesn't exist
167
+ this.handleOnBeforeRowDetailToggle(event, args);
168
+
169
+ if (typeof this.rowDetailViewOptions?.onBeforeRowDetailToggle === 'function') {
170
+ return this.rowDetailViewOptions.onBeforeRowDetailToggle(event, args);
171
+ }
172
+ return true;
173
+ });
174
+
175
+ this._eventHandler.subscribe(this.onRowBackToViewportRange, async (event, args) => {
176
+ // when row is back to viewport range, we will re-render the View Component(s)
177
+ await this.handleOnRowBackToViewportRange(event, args);
178
+
179
+ if (typeof this.rowDetailViewOptions?.onRowBackToViewportRange === 'function') {
180
+ this.rowDetailViewOptions.onRowBackToViewportRange(event, args);
181
+ }
182
+ });
183
+
184
+ this._eventHandler.subscribe(this.onBeforeRowOutOfViewportRange, (event, args) => {
185
+ if (typeof this.rowDetailViewOptions?.onBeforeRowOutOfViewportRange === 'function') {
186
+ this.rowDetailViewOptions.onBeforeRowOutOfViewportRange(event, args);
187
+ }
188
+ this.disposeView(args.item);
189
+ });
190
+
191
+ this._eventHandler.subscribe(this.onRowOutOfViewportRange, (event, args) => {
192
+ if (typeof this.rowDetailViewOptions?.onRowOutOfViewportRange === 'function') {
193
+ this.rowDetailViewOptions.onRowOutOfViewportRange(event, args);
194
+ }
195
+ });
197
196
 
198
197
  // --
199
198
  // hook some events needed by the Plugin itself
@@ -223,18 +222,29 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView {
223
222
 
224
223
  /** Redraw (re-render) all the expanded row detail View Components */
225
224
  async redrawAllViewComponents() {
226
- await Promise.all(this._views.map(async x => this.redrawViewComponent(x)));
225
+ this.resetRenderedRows();
226
+ const promises: Promise<void>[] = [];
227
+ this._views.forEach((view) => {
228
+ if (!view.rendered) {
229
+ promises.push(this.redrawViewComponent(view))
230
+ }
231
+ });
232
+ await Promise.all(promises);
227
233
  }
228
234
 
229
235
  /** Render all the expanded row detail View Components */
230
236
  async renderAllViewModels() {
231
- await Promise.all(this._views.filter(x => x?.dataContext).map(async x => this.renderViewModel(x.dataContext)));
237
+ const promises: Promise<void>[] = [];
238
+ Array.from(this._views)
239
+ .filter((x) => x?.dataContext)
240
+ .forEach((x) => promises.push(this.renderViewModel(x.dataContext)));
241
+ await Promise.all(promises);
232
242
  }
233
243
 
234
244
  /** Redraw the necessary View Component */
235
245
  async redrawViewComponent(view: CreatedView) {
236
- const containerElement = this.gridContainerElement.getElementsByClassName(`${ROW_DETAIL_CONTAINER_PREFIX}${view.id}`);
237
- if (containerElement?.length >= 0) {
246
+ const containerElements = this.gridContainerElement.getElementsByClassName(`${ROW_DETAIL_CONTAINER_PREFIX}${view.id}`);
247
+ if (containerElements?.length >= 0) {
238
248
  await this.renderViewModel(view.dataContext);
239
249
  }
240
250
  }
@@ -243,15 +253,19 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView {
243
253
  async renderPreloadView(item: any) {
244
254
  const containerElements = this.gridContainerElement.getElementsByClassName(`${PRELOAD_CONTAINER_PREFIX}`);
245
255
  if (this._preloadComponent && containerElements?.length) {
256
+ // render row detail
257
+ const bindableData = {
258
+ model: item,
259
+ addon: this,
260
+ grid: this._grid,
261
+ dataView: this.dataView,
262
+ parent: this.rowDetailViewOptions?.parent,
263
+ } as ViewModelBindableInputData;
246
264
  const detailContainer = document.createElement('section');
247
265
  containerElements[containerElements.length - 1]!.appendChild(detailContainer);
248
266
 
249
- const { root } = await loadReactComponentDynamically(this._preloadComponent, detailContainer as HTMLElement);
250
- const viewObj = this._views.find(obj => obj.id === item[this.datasetIdPropName]);
251
- this._root = root;
252
- if (viewObj) {
253
- viewObj.root = root;
254
- }
267
+ const { root } = await createReactComponentDynamically(this._preloadComponent, detailContainer as HTMLElement, bindableData);
268
+ this._preloadRoot = root;
255
269
  }
256
270
  }
257
271
 
@@ -259,6 +273,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView {
259
273
  async renderViewModel(item: any) {
260
274
  const containerElements = this.gridContainerElement.getElementsByClassName(`${ROW_DETAIL_CONTAINER_PREFIX}${item[this.datasetIdPropName]}`);
261
275
  if (this._component && containerElements?.length) {
276
+ // render row detail
262
277
  const bindableData = {
263
278
  model: item,
264
279
  addon: this,
@@ -268,11 +283,21 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView {
268
283
  } as ViewModelBindableInputData;
269
284
  const viewObj = this._views.find(obj => obj.id === item[this.datasetIdPropName]);
270
285
 
286
+ // remove any previous mounted views, if found then unmount them and delete them from our references array
287
+ // const viewIdx = this._views.findIndex((obj) => obj.id === item[this.datasetIdPropName]);
288
+ // if (this._views[viewIdx]?.root) {
289
+ // this._views[viewIdx].root.unmount();
290
+ // this._views.splice(viewIdx, 1);
291
+ // }
292
+
271
293
  // load our Row Detail React Component dynamically, typically we would want to use `root.render()` after the preload component (last argument below)
272
294
  // BUT the root render doesn't seem to work and shows a blank element, so we'll use `createRoot()` every time even though it shows a console log in Dev
273
295
  // that is the only way I got it working so let's use it anyway and console warnings are removed in production anyway
274
- const { root } = await loadReactComponentDynamically(this._component, containerElements[containerElements.length - 1] as HTMLElement, bindableData /*, viewObj?.root */);
275
- if (!viewObj) {
296
+ const { root } = createReactComponentDynamically(this._component, containerElements[containerElements.length - 1] as HTMLElement, bindableData /*, viewObj?.root */);
297
+ if (viewObj) {
298
+ viewObj.root = root;
299
+ viewObj.rendered = true;
300
+ } else {
276
301
  this.addViewInfoToViewsRef(item, root);
277
302
  }
278
303
  }
@@ -286,21 +311,28 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView {
286
311
  const viewInfo: CreatedView = {
287
312
  id: item[this.datasetIdPropName],
288
313
  dataContext: item,
289
- root
314
+ root,
315
+ rendered: !!root,
290
316
  };
291
- const idPropName = this.gridOptions.datasetIdPropertyName || 'id';
292
- addToArrayWhenNotExists(this._views, viewInfo, idPropName);
317
+ addToArrayWhenNotExists(this._views, viewInfo, this.datasetIdPropName);
318
+ }
319
+
320
+ protected disposeView(item: any, removeFromArray = false): void {
321
+ const foundViewIdx = this._views.findIndex((view: CreatedView) => view.id === item[this.datasetIdPropName]);
322
+ if (foundViewIdx >= 0 && this.disposeViewComponent(this._views[foundViewIdx])) {
323
+ if (removeFromArray) {
324
+ this._views.splice(foundViewIdx, 1);
325
+ }
326
+ }
293
327
  }
294
328
 
295
329
  protected disposeViewComponent(expandedView: CreatedView): CreatedView | void {
296
- if (expandedView) {
297
- if (expandedView?.root) {
298
- const container = this.gridContainerElement.getElementsByClassName(`${ROW_DETAIL_CONTAINER_PREFIX}${this._views[0].id}`);
299
- if (container?.length) {
300
- expandedView.root.unmount();
301
- container[0].textContent = '';
302
- return expandedView;
303
- }
330
+ if (expandedView?.root) {
331
+ const container = this.gridContainerElement.getElementsByClassName(`${ROW_DETAIL_CONTAINER_PREFIX}${expandedView.id}`);
332
+ if (container?.length) {
333
+ expandedView.root.unmount();
334
+ container[0].textContent = '';
335
+ return expandedView;
304
336
  }
305
337
  }
306
338
  }
@@ -318,10 +350,7 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView {
318
350
  this.addViewInfoToViewsRef(args.item, null);
319
351
  } else {
320
352
  // collapsing, so dispose of the View
321
- const foundViewIdx = this._views.findIndex((view: CreatedView) => view.id === args.item[this.datasetIdPropName]);
322
- if (foundViewIdx >= 0 && this.disposeViewComponent(this._views[foundViewIdx])) {
323
- this._views.splice(foundViewIdx, 1);
324
- }
353
+ this.disposeView(args.item, true);
325
354
  }
326
355
  }
327
356
 
@@ -334,8 +363,9 @@ export class SlickRowDetailView extends UniversalSlickRowDetailView {
334
363
  rowIdsOutOfViewport: (string | number)[];
335
364
  grid: SlickGrid;
336
365
  }) {
337
- if (args?.item) {
338
- await this.redrawAllViewComponents();
366
+ const viewModel = Array.from(this._views).find((x) => x.id === args.rowId);
367
+ if (viewModel) {
368
+ this.redrawViewComponent(viewModel);
339
369
  }
340
370
  }
341
371
 
@@ -235,7 +235,6 @@ export const GlobalGridOptions: Partial<GridOption> = {
235
235
  panelRows: 1,
236
236
  keyPrefix: '__',
237
237
  useRowClick: false,
238
- useSimpleViewportCalc: true,
239
238
  saveDetailViewOnScroll: false,
240
239
  } as RowDetailView,
241
240
  headerRowHeight: 35,
@@ -2,14 +2,22 @@ import React from 'react';
2
2
  import { createRef } from 'react';
3
3
  import { createRoot, type Root } from 'react-dom/client';
4
4
 
5
+ // these 2 functions are the same except that 1 is synch and the promise is delayed by a CPU cycle before resolving
6
+
7
+ export function createReactComponentDynamically<T = any>(customComponent: any, targetElm: HTMLElement, props?: any, root?: Root | null): { component: T, root: Root } {
8
+ const compRef = createRef();
9
+ root ??= createRoot(targetElm);
10
+ root.render(React.createElement(customComponent, { ...props, ref: compRef }));
11
+
12
+ return { component: compRef.current as T, root: root as Root };
13
+ }
14
+
5
15
  export function loadReactComponentDynamically<T = any>(customComponent: any, targetElm: HTMLElement, props?: any, root?: Root | null): Promise<{ component: T, root: Root }> {
6
16
  return new Promise(resolve => {
7
17
  const compRef = createRef();
8
18
  root ??= createRoot(targetElm);
9
19
  root.render(React.createElement(customComponent, { ...props, ref: compRef }));
10
20
 
11
- queueMicrotask(() => {
12
- resolve({ component: compRef.current as T, root: root as Root });
13
- });
21
+ queueMicrotask(() => resolve({ component: compRef.current as T, root: root as Root }));
14
22
  });
15
23
  }