Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8c67a89

Browse files
committed
angular wrapper - serialize custom data support
* fixed to make sure dynamic components are destroyed * added support for saving/loading custom data per widget
1 parent cdf7796 commit 8c67a89

File tree

8 files changed

+141
-48
lines changed

8 files changed

+141
-48
lines changed

angular/projects/demo/src/app/app.component.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { AngularNgForTestComponent } from './ngFor';
55
import { AngularNgForCmdTestComponent } from './ngFor_cmd';
66

77
// NOTE: local testing of file
8-
// import { GridstackComponent, NgGridStackOptions, NgGridStackWidget, elementCB, gsCreateNgComponents, nodesCB } from '../../../../../dist/angular';
8+
// import { GridstackComponent, NgGridStackOptions, NgGridStackWidget, elementCB, gsCreateNgComponents, nodesCB } from './gridstack.component';
99
import { GridstackComponent, NgGridStackOptions, NgGridStackWidget, elementCB, gsCreateNgComponents, nodesCB } from 'gridstack/dist/angular';
1010

1111
// unique ids sets for each item for correct ngFor updating
@@ -25,7 +25,7 @@ export class AppComponent implements OnInit {
2525
@ViewChild('textArea', {static: true}) textEl?: ElementRef<HTMLTextAreaElement>;
2626

2727
// which sample to show
28-
public show = 6;
28+
public show = 5;
2929

3030
/** sample grid options and items to load... */
3131
public items: GridStackWidget[] = [
@@ -38,9 +38,9 @@ export class AppComponent implements OnInit {
3838
float: true,
3939
minRow: 1,
4040
}
41-
public gridOptionsFull: GridStackOptions = {
41+
public gridOptionsFull: NgGridStackOptions = {
4242
...this.gridOptions,
43-
children: this.items,
43+
children: [{x:0, y:0, selector:'app-a'}, {x:1, y:0, selector:'app-b'}, {x:2, y:0, content:'plain html'}],
4444
}
4545

4646
// nested grid options
@@ -50,7 +50,7 @@ export class AppComponent implements OnInit {
5050
acceptWidgets: true, // will accept .grid-stack-item by default
5151
margin: 5,
5252
};
53-
private sub1: NgGridStackWidget[] = [ {x:0, y:0, type:'app-a'}, {x:1, y:0, type:'app-b'}, {x:2, y:0, type:'app-c'}, {x:3, y:0}, {x:0, y:1}, {x:1, y:1}];
53+
private sub1: NgGridStackWidget[] = [ {x:0, y:0, selector:'app-a'}, {x:1, y:0, selector:'app-b'}, {x:2, y:0, selector:'app-c'}, {x:3, y:0}, {x:0, y:1}, {x:1, y:1}];
5454
private sub2: NgGridStackWidget[] = [ {x:0, y:0}, {x:0, y:1, w:2}];
5555
private subChildren: NgGridStackWidget[] = [
5656
{x:0, y:0, content: 'regular item'},
@@ -71,7 +71,7 @@ export class AppComponent implements OnInit {
7171
constructor() {
7272
// give them content and unique id to make sure we track them during changes below...
7373
[...this.items, ...this.subChildren, ...this.sub1, ...this.sub2].forEach((w: NgGridStackWidget) => {
74-
if (!w.type && !w.subGridOpts) w.content = `item ${ids}`;
74+
if (!w.selector && !w.subGridOpts) w.content = `item ${ids}`;
7575
w.id = String(ids++);
7676
});
7777
}

angular/projects/demo/src/app/app.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { AngularSimpleComponent } from './simple';
88
import { AComponent, BComponent, CComponent } from './dummy.component';
99

1010
// local testing
11-
// import { GridstackModule, GridstackComponent } from '../../../../../dist/angular';
11+
// import { GridstackModule } from './gridstack.module';
12+
// import { GridstackComponent } from './gridstack.component';
1213
import { GridstackModule, GridstackComponent } from 'gridstack/dist/angular';
1314

1415
@NgModule({

angular/projects/demo/src/app/dummy.component.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,31 @@
55

66
// dummy testing component that will be grid items content
77

8-
import { Component } from '@angular/core';
8+
import { Component, OnDestroy, Input } from '@angular/core';
9+
import { BaseWidget, NgCompInputs } from 'gridstack/dist/angular';
910

1011
@Component({
1112
selector: 'app-a',
12-
template: 'Comp A',
13+
template: 'Comp A {{text}}',
1314
})
14-
export class AComponent {
15+
export class AComponent extends BaseWidget implements OnDestroy {
16+
@Input() text: string = 'foo'; // test custom input data
17+
public override serialize(): NgCompInputs | undefined { return this.text ? {text: this.text} : undefined; }
18+
ngOnDestroy() {
19+
console.log('Comp A destroyed'); // test to make sure cleanup happens
20+
}
1521
}
1622

1723
@Component({
1824
selector: 'app-b',
1925
template: 'Comp B',
2026
})
21-
export class BComponent {
27+
export class BComponent extends BaseWidget {
2228
}
2329

2430
@Component({
2531
selector: 'app-c',
2632
template: 'Comp C',
2733
})
28-
export class CComponent {
34+
export class CComponent extends BaseWidget {
2935
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* gridstack-item.component.ts 8.1.2-dev
3+
* Copyright (c) 2022 Alain Dumesny - see GridStack root license
4+
*/
5+
6+
/**
7+
* Base interface that all widgets need to implement in order for GridstackItemComponent to correctly save/load/delete/..
8+
*/
9+
10+
import { Injectable } from '@angular/core';
11+
import { NgCompInputs, NgGridStackWidget } from './gridstack.component';
12+
13+
@Injectable()
14+
export abstract class BaseWidget {
15+
/**
16+
* REDEFINE to return an object representing the data needed to re-create yourself, other than `selector` already handled.
17+
* This should map directly to the @Input() fields of this objects on create, so a simple apply can be used on read
18+
*/
19+
public serialize(): NgCompInputs | undefined { return; }
20+
21+
/**
22+
* REDEFINE this if your widget needs to read from saved data and transform it to create itself - you do this for
23+
* things that are not mapped directly into @Input() fields for example.
24+
*/
25+
public deserialize(w: NgGridStackWidget) {
26+
if (w.input) Object.assign(this, w.input);
27+
}
28+
}

angular/projects/lib/src/lib/gridstack-item.component.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
* Copyright (c) 2022 Alain Dumesny - see GridStack root license
44
*/
55

6-
import { Component, ElementRef, Input, ViewChild, ViewContainerRef, OnDestroy } from '@angular/core';
6+
import { Component, ElementRef, Input, ViewChild, ViewContainerRef, OnDestroy, ComponentRef } from '@angular/core';
77
import { GridItemHTMLElement, GridStackNode } from 'gridstack';
8+
import { BaseWidget } from './base-widgets';
89

910
/** store element to Ng Class pointer back */
1011
export interface GridItemCompHTMLElement extends GridItemHTMLElement {
@@ -35,6 +36,12 @@ export class GridstackItemComponent implements OnDestroy {
3536
/** container to append items dynamically */
3637
@ViewChild('container', { read: ViewContainerRef, static: true}) public container?: ViewContainerRef;
3738

39+
/** ComponentRef of ourself - used by dynamic object to correctly get removed */
40+
public ref: ComponentRef<GridstackItemComponent> | undefined;
41+
42+
/** child component so we can save/restore additional data to be saved along */
43+
public childWidget: BaseWidget | undefined;
44+
3845
/** list of options for creating/updating this item */
3946
@Input() public set options(val: GridStackNode) {
4047
if (this.el.gridstackNode?.grid) {
@@ -65,6 +72,7 @@ export class GridstackItemComponent implements OnDestroy {
6572
}
6673

6774
public ngOnDestroy(): void {
75+
delete this.ref;
6876
delete this.el._gridItemComp;
6977
}
7078
}

angular/projects/lib/src/lib/gridstack.component.ts

Lines changed: 82 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,29 @@
44
*/
55

66
import { AfterContentInit, Component, ContentChildren, ElementRef, EventEmitter, Input,
7-
OnDestroy, OnInit, Output, QueryList, Type, ViewChild, ViewContainerRef, reflectComponentType } from '@angular/core';
7+
OnDestroy, OnInit, Output, QueryList, Type, ViewChild, ViewContainerRef, reflectComponentType, ComponentRef } from '@angular/core';
88
import { Subject } from 'rxjs';
99
import { takeUntil } from 'rxjs/operators';
1010
import { GridHTMLElement, GridItemHTMLElement, GridStack, GridStackNode, GridStackOptions, GridStackWidget } from 'gridstack';
1111

1212
import { GridItemCompHTMLElement, GridstackItemComponent } from './gridstack-item.component';
13+
import { BaseWidget } from './base-widgets';
1314

1415
/** events handlers emitters signature for different events */
1516
export type eventCB = {event: Event};
1617
export type elementCB = {event: Event, el: GridItemHTMLElement};
1718
export type nodesCB = {event: Event, nodes: GridStackNode[]};
1819
export type droppedCB = {event: Event, previousNode: GridStackNode, newNode: GridStackNode};
1920

21+
export type NgCompInputs = {[key: string]: any};
22+
2023
/** extends to store Ng Component selector, instead/inAddition to content */
2124
export interface NgGridStackWidget extends GridStackWidget {
22-
type?: string; // component type to create as content
25+
selector?: string; // component type to create as content
26+
input?: NgCompInputs; // serialized data for the component input fields
2327
}
2428
export interface NgGridStackNode extends GridStackNode {
25-
type?: string; // component type to create as content
29+
selector?: string; // component type to create as content
2630
}
2731
export interface NgGridStackOptions extends GridStackOptions {
2832
children?: NgGridStackWidget[];
@@ -96,6 +100,9 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
96100
/** return the GridStack class */
97101
public get grid(): GridStack | undefined { return this._grid; }
98102

103+
/** ComponentRef of ourself - used by dynamic object to correctly get removed */
104+
public ref: ComponentRef<GridstackComponent> | undefined;
105+
99106
/**
100107
* stores the selector -> Type mapping, so we can create items dynamically from a string.
101108
* Unfortunately Ng doesn't provide public access to that mapping.
@@ -107,8 +114,7 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
107114
}
108115
/** return the ng Component selector */
109116
public static getSelector(type: Type<Object>): string {
110-
const mirror = reflectComponentType(type)!;
111-
return mirror.selector;
117+
return reflectComponentType(type)!.selector;
112118
}
113119

114120
private _options?: GridStackOptions;
@@ -145,6 +151,7 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
145151
}
146152

147153
public ngOnDestroy(): void {
154+
delete this.ref;
148155
this.ngUnsubscribe.next();
149156
this.ngUnsubscribe.complete();
150157
this.grid?.destroy();
@@ -197,44 +204,84 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
197204
/**
198205
* can be used when a new item needs to be created, which we do as a Angular component, or deleted (skip)
199206
**/
200-
export function gsCreateNgComponents(host: GridCompHTMLElement | HTMLElement, w: NgGridStackWidget | GridStackOptions, add: boolean, isGrid: boolean): HTMLElement | undefined {
201-
// only care about creating ng components here...
202-
if (!add || !host) return;
203-
204-
// create the component dynamically - see https://angular.io/docs/ts/latest/cookbook/dynamic-component-loader.html
205-
if (isGrid) {
206-
let grid: GridstackComponent | undefined;
207-
const gridItemComp = (host.parentElement as GridItemCompHTMLElement)?._gridItemComp;
208-
if (gridItemComp) {
209-
grid = gridItemComp.container?.createComponent(GridstackComponent)?.instance;
210-
} else {
207+
export function gsCreateNgComponents(host: GridCompHTMLElement | HTMLElement, w: NgGridStackWidget | GridStackNode, add: boolean, isGrid: boolean): HTMLElement | undefined {
208+
if (add) {
209+
//
210+
// create the component dynamically - see https://angular.io/docs/ts/latest/cookbook/dynamic-component-loader.html
211+
//
212+
if (!host) return;
213+
if (isGrid) {
214+
const container = (host.parentElement as GridItemCompHTMLElement)?._gridItemComp?.container;
211215
// TODO: figure out how to create ng component inside regular Div. need to access app injectors...
212-
// const hostElement: Element = host;
213-
// const environmentInjector: EnvironmentInjector;
214-
// grid = createComponent(GridstackComponent, {environmentInjector, hostElement})?.instance;
216+
// if (!container) {
217+
// const hostElement: Element = host;
218+
// const environmentInjector: EnvironmentInjector;
219+
// grid = createComponent(GridstackComponent, {environmentInjector, hostElement})?.instance;
220+
// }
221+
const gridRef = container?.createComponent(GridstackComponent);
222+
const grid = gridRef?.instance;
223+
if (!grid) return;
224+
grid.ref = gridRef;
225+
grid.options = w as GridStackOptions;
226+
return grid.el;
227+
} else {
228+
const gridComp = (host as GridCompHTMLElement)._gridComp;
229+
const gridItemRef = gridComp?.container?.createComponent(GridstackItemComponent);
230+
const gridItem = gridItemRef?.instance;
231+
if (!gridItem) return;
232+
gridItem.ref = gridItemRef
233+
234+
// IFF we're not a subGrid, define what type of component to create as child, OR you can do it GridstackItemComponent template, but this is more generic
235+
const selector = (w as NgGridStackWidget).selector;
236+
const type = selector ? GridstackComponent.selectorToType[selector] : undefined;
237+
if (!w.subGridOpts && type) {
238+
const childWidget = gridItem.container?.createComponent(type)?.instance as BaseWidget;
239+
if (typeof childWidget?.serialize === 'function' && typeof childWidget?.deserialize === 'function') {
240+
// proper BaseWidget subclass, save it and load additional data
241+
gridItem.childWidget = childWidget;
242+
childWidget.deserialize(w);
243+
}
244+
}
245+
246+
return gridItem.el;
215247
}
216-
if (grid) grid.options = w as GridStackOptions;
217-
return grid?.el;
218248
} else {
219-
const gridComp = (host as GridCompHTMLElement)._gridComp;
220-
const gridItem = gridComp?.container?.createComponent(GridstackItemComponent)?.instance;
221-
222-
// IFF we're not a subGrid, define what type of component to create as child, OR you can do it GridstackItemComponent template, but this is more generic
223-
const selector = (w as NgGridStackWidget).type;
224-
const type = selector ? GridstackComponent.selectorToType[selector] : undefined;
225-
if (!w.subGridOpts && type) {
226-
gridItem?.container?.createComponent(type);
249+
//
250+
// REMOVE - have to call ComponentRef:destroy() for dynamic objects to correctly remove themselves
251+
// Note: this will destroy all children dynamic components as well: gridItem -> childWidget
252+
//
253+
const n = w as GridStackNode;
254+
if (isGrid) {
255+
const grid = (n.el as GridCompHTMLElement)?._gridComp;
256+
if (grid?.ref) grid.ref.destroy();
257+
else grid?.ngOnDestroy();
258+
} else {
259+
const gridItem = (n.el as GridItemCompHTMLElement)?._gridItemComp;
260+
if (gridItem?.ref) gridItem.ref.destroy();
261+
else gridItem?.ngOnDestroy();
227262
}
228-
229-
return gridItem?.el;
230263
}
264+
return;
231265
}
232266

233267
/**
234-
* can be used when saving the grid - make sure we save the content from the field (not HTML as we get ng markups)
235-
* and can put the extra info of type, otherwise content
268+
* called for each item in the grid - check if additional information needs to be saved.
269+
* Note: since this is options minus gridstack private members using Utils.removeInternalForSave(),
270+
* this typically doesn't need to do anything. However your custom Component @Input() are now supported
271+
* using BaseWidget.serialize()
236272
*/
237273
export function gsSaveAdditionalNgInfo(n: NgGridStackNode, w: NgGridStackWidget) {
238-
if (n.type) w.type = n.type;
239-
else if (n.content) w.content = n.content;
274+
const gridItem = (n.el as GridItemCompHTMLElement)?._gridItemComp;
275+
if (gridItem) {
276+
const input = gridItem.childWidget?.serialize();
277+
if (input) {
278+
w.input = input;
279+
}
280+
return;
281+
}
282+
// else check if Grid
283+
const grid = (n.el as GridCompHTMLElement)?._gridComp;
284+
if (grid) {
285+
//.... save any custom data
286+
}
240287
}

angular/projects/lib/src/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44

55
export * from './lib/gridstack-item.component';
66
export * from './lib/gridstack.component';
7+
export * from './lib/base-widgets';
78
export * from './lib/gridstack.module';

doc/CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ Change log
9090

9191
## 8.1.2-dev TBD
9292
* feat: `makeWidget()` now take optional `GridStackWidget` for sizing
93+
* fix: make sure `GridStack.saveCB` is call in `removeWidget()`
94+
* feat: angular wrapper: serialize custom data support, and making sure destroy() is called on ng components
9395

9496
## 8.1.2 (2023-5-22)
9597
* [#2323](https://github.com/gridstack/gridstack.js/issues/2323) module for Angular wrapper

0 commit comments

Comments
 (0)