Skip to content

Commit a7a4c8b

Browse files
authored
Merge pull request #2251 from adumesny/master
feature: Angular nested grid
2 parents 088d963 + 4d6d5cd commit a7a4c8b

File tree

9 files changed

+134
-49
lines changed

9 files changed

+134
-49
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
.test-container {
22
margin-top: 30px;
33
}
4+
button.active {
5+
color: #fff;
6+
background-color: #007bff;
7+
}

demo/angular/src/app/app.component.html

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11

2-
<div>
2+
<div class="button-container">
33
<p class="pick-info">Pick a demo to load:</p>
4-
<button (click)="show=0">Simple</button>
5-
<button (click)="show=1">ngFor case</button>
6-
<button (click)="show=2">ngFor custom command</button>
7-
<button (click)="show=3">Component HTML template</button>
8-
<button (click)="show=4">Component ngFor</button>
9-
<button (click)="show=5">Component Dynamic</button>
4+
<button (click)="show=0" [class.active]="show===0">Simple</button>
5+
<button (click)="show=1" [class.active]="show===1">ngFor case</button>
6+
<button (click)="show=2" [class.active]="show===2">ngFor custom command</button>
7+
<button (click)="show=3" [class.active]="show===3">Component HTML template</button>
8+
<button (click)="show=4" [class.active]="show===4">Component ngFor</button>
9+
<button (click)="show=5" [class.active]="show===5">Component Dynamic</button>
10+
<button (click)="show=6" [class.active]="show===6">Nested Grid</button>
1011
</div>
1112

1213
<div class="test-container">
@@ -49,4 +50,15 @@
4950
</gridstack>
5051
</div>
5152

53+
54+
<div *ngIf="show===6" >
55+
<p><b>Nested Grid</b>: shows nested component grids, like nested.html demo but with Ng Components</p>
56+
<button (click)="add(gridComp)">add item</button>
57+
<button (click)="delete(gridComp)">remove item</button>
58+
<button (click)="modify(gridComp)">modify item</button>
59+
<button (click)="newLayout(gridComp)">new layout</button>
60+
<gridstack #gridComp [options]="nestedGridOptions" (changeCB)="onChange($event)" (resizeStopCB)="onResizeStop($event)">
61+
</gridstack>
62+
</div>
63+
5264
</div>

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,35 @@ export class AppComponent {
2929
children: this.items,
3030
}
3131

32+
// nested grid options
33+
public sub1: GridStackWidget[] = [ {x:0, y:0}, {x:1, y:0}, {x:2, y:0}, {x:3, y:0}, {x:0, y:1}, {x:1, y:1}];
34+
public sub2: GridStackWidget[] = [ {x:0, y:0}, {x:0, y:1, w:2}];
35+
public subOptions: GridStackOptions = {
36+
cellHeight: 50, // should be 50 - top/bottom
37+
column: 'auto', // size to match container. make sure to include gridstack-extra.min.css
38+
acceptWidgets: true, // will accept .grid-stack-item by default
39+
margin: 5,
40+
};
41+
public nestedGridOptions: GridStackOptions = { // main grid options
42+
cellHeight: 50,
43+
margin: 5,
44+
minRow: 2, // don't collapse when empty
45+
disableOneColumnMode: true,
46+
acceptWidgets: true,
47+
id: 'main',
48+
children: [
49+
{x:0, y:0, content: 'regular item', id: 0},
50+
{x:1, y:0, w:4, h:4, subGrid: {children: this.sub1, id:'sub1_grid', class: 'sub1', ...this.subOptions}},
51+
{x:5, y:0, w:3, h:4, subGrid: {children: this.sub2, id:'sub2_grid', class: 'sub2', ...this.subOptions}},
52+
]
53+
};
54+
3255
constructor() {
3356
// give them content and unique id to make sure we track them during changes below...
34-
this.items.forEach(w => {
57+
[...this.items, ...this.sub1, ...this.sub2].forEach(w => {
3558
w.content = `item ${ids}`;
3659
w.id = String(ids++);
37-
})
60+
});
3861
}
3962

4063
/** called whenever items change size/position/etc.. */
@@ -72,7 +95,7 @@ export class AppComponent {
7295
}
7396

7497
/**
75-
* TEST TEMPLATE operations for ngFor case - NOT recommended unless you have no GS creating/re-parenting
98+
* ngFor case: TEST TEMPLATE operations - NOT recommended unless you have no GS creating/re-parenting
7699
*/
77100
public addNgFor() {
78101
// new array isn't required as Angular detects changes to content with trackBy:identify()

demo/angular/src/app/gridstack-item.component.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,36 @@
33
* Copyright (c) 2022 Alain Dumesny - see GridStack root license
44
*/
55

6-
import { ChangeDetectionStrategy, Component, ElementRef, Input } from '@angular/core';
6+
import { ChangeDetectionStrategy, Component, ElementRef, Input, ViewChild, ViewContainerRef, OnDestroy } from '@angular/core';
77
import { GridItemHTMLElement, GridStackNode } from 'gridstack';
88

9+
/** store element to Ng Class pointer back */
10+
export interface GridItemCompHTMLElement extends GridItemHTMLElement {
11+
_gridItemComp?: GridstackItemComponent;
12+
}
13+
914
/**
1015
* HTML Component Wrapper for gridstack items, in combination with GridstackComponent for parent grid
1116
*/
1217
@Component({
1318
selector: 'gridstack-item',
1419
template: `
1520
<div class="grid-stack-item-content">
21+
<!-- this is where you would create the right component based on some internal type or id. doing .content for demo purpose -->
1622
{{options.content}}
1723
<ng-content></ng-content>
24+
<!-- where dynamic items go (like sub-grids) -->
25+
<ng-template #container></ng-template>
1826
</div>`,
1927
styles: [`
2028
:host { display: block; }
2129
`],
2230
changeDetection: ChangeDetectionStrategy.OnPush,
2331
})
24-
export class GridstackItemComponent {
32+
export class GridstackItemComponent implements OnDestroy {
33+
34+
/** container to append items dynamically */
35+
@ViewChild('container', { read: ViewContainerRef, static: true}) public container?: ViewContainerRef;
2536

2637
/** list of options for creating/updating this item */
2738
@Input() public set options(val: GridStackNode) {
@@ -42,13 +53,18 @@ export class GridstackItemComponent {
4253
private _options?: GridStackNode;
4354

4455
/** return the native element that contains grid specific fields as well */
45-
public get el(): GridItemHTMLElement { return this.elementRef.nativeElement; }
56+
public get el(): GridItemCompHTMLElement { return this.elementRef.nativeElement; }
4657

4758
/** clears the initial options now that we've built */
4859
public clearOptions() {
4960
delete this._options;
5061
}
5162

5263
constructor(private readonly elementRef: ElementRef<GridItemHTMLElement>) {
64+
this.el._gridItemComp = this;
65+
}
66+
67+
public ngOnDestroy(): void {
68+
delete this.el._gridItemComp;
5369
}
5470
}

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

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,21 @@ import { AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren,
77
NgZone, OnDestroy, OnInit, Output, QueryList, ViewChild, ViewContainerRef } from '@angular/core';
88
import { Subject } from 'rxjs';
99
import { takeUntil } from 'rxjs/operators';
10-
import { AddRemoveFcn, GridHTMLElement, GridItemHTMLElement, GridStack, GridStackNode, GridStackOptions, GridStackWidget } from 'gridstack';
10+
import { GridHTMLElement, GridItemHTMLElement, GridStack, GridStackNode, GridStackOptions, GridStackWidget } from 'gridstack';
1111

12-
import { GridstackItemComponent } from './gridstack-item.component';
12+
import { GridItemCompHTMLElement, GridstackItemComponent } from './gridstack-item.component';
1313

1414
/** events handlers emitters signature for different events */
1515
export type eventCB = {event: Event};
1616
export type elementCB = {event: Event, el: GridItemHTMLElement};
1717
export type nodesCB = {event: Event, nodes: GridStackNode[]};
1818
export type droppedCB = {event: Event, previousNode: GridStackNode, newNode: GridStackNode};
1919

20+
/** store element to Ng Class pointer back */
21+
export interface GridCompHTMLElement extends GridHTMLElement {
22+
_gridComp?: GridstackComponent;
23+
}
24+
2025
/**
2126
* HTML Component Wrapper for gridstack, in combination with GridstackItemComponent for the items
2227
*/
@@ -71,28 +76,27 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
7176
@Output() public resizeStopCB = new EventEmitter<elementCB>();
7277

7378
/** return the native element that contains grid specific fields as well */
74-
public get el(): GridHTMLElement { return this.elementRef.nativeElement; }
79+
public get el(): GridCompHTMLElement { return this.elementRef.nativeElement; }
7580

7681
/** return the GridStack class */
7782
public get grid(): GridStack | undefined { return this._grid; }
7883

7984
private _options?: GridStackOptions;
8085
private _grid?: GridStack;
8186
private loaded?: boolean;
82-
private outsideAddRemove?: AddRemoveFcn;
8387
private ngUnsubscribe: Subject<void> = new Subject();
8488

8589
constructor(
8690
private readonly zone: NgZone,
87-
private readonly elementRef: ElementRef<GridHTMLElement>,
91+
private readonly elementRef: ElementRef<GridCompHTMLElement>,
8892
) {
93+
this.el._gridComp = this;
8994
}
9095

9196
public ngOnInit(): void {
9297
// inject our own addRemove so we can create GridItemComponent instead of simple divs
9398
const opts: GridStackOptions = this._options || {};
94-
if (opts.addRemoveCB) this.outsideAddRemove = opts.addRemoveCB;
95-
opts.addRemoveCB = this._addRemoveCB.bind(this);
99+
opts.addRemoveCB = GridstackComponent._addRemoveCB;
96100

97101
// init ourself before any template children are created since we track them below anyway - no need to double create+update widgets
98102
this.loaded = !!this.options?.children?.length;
@@ -118,6 +122,7 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
118122
this.ngUnsubscribe.complete();
119123
this.grid?.destroy();
120124
delete this._grid;
125+
delete this.el._gridComp;
121126
}
122127

123128
/**
@@ -159,14 +164,22 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
159164
}
160165

161166
/** called by GS when a new item needs to be created, which we do as a Angular component, or deleted (skip) */
162-
private _addRemoveCB(g: GridStack, w: GridStackWidget, add: boolean): HTMLElement | undefined {
167+
private static _addRemoveCB(parent: GridCompHTMLElement | HTMLElement, w: GridStackWidget | GridStackOptions, add: boolean, isGrid: boolean): HTMLElement | undefined {
163168
if (add) {
164-
if (!this.container) return;
169+
if (!parent) return;
165170
// create the grid item dynamically - see https://angular.io/docs/ts/latest/cookbook/dynamic-component-loader.html
166-
const gridItem = this.container.createComponent(GridstackItemComponent)?.instance;
167-
return gridItem?.el;
171+
if (isGrid) {
172+
const gridItemComp = (parent.parentElement as GridItemCompHTMLElement)._gridItemComp;
173+
const grid = gridItemComp?.container?.createComponent(GridstackComponent)?.instance;
174+
if (grid) grid.options = w as GridStackOptions;
175+
return grid?.el;
176+
} else {
177+
// TODO: use GridStackWidget to define what type of component to create as child, or do it in GridstackItemComponent template...
178+
const gridComp = (parent as GridCompHTMLElement)._gridComp;
179+
const gridItem = gridComp?.container?.createComponent(GridstackItemComponent)?.instance;
180+
return gridItem?.el;
181+
}
168182
}
169-
// if (this.outsideAddRemove) this.outsideAddRemove(g, w, add); // TODO: ?
170183
return;
171184
}
172185
}

demo/angular/src/styles.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
/* re-use existing demo css file we already use for the plain demos - that include gridstack.css which is required */
22
@import "../../demo.css";
3+
/* required file for gridstack 2-11 column */
4+
@import "../../../dist/gridstack-extra.css";

doc/CHANGES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ Change log
8383
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
8484

8585
## 7.2.3-dev (TBD)
86-
* feat [#2229](https://github.com/gridstack/gridstack.js/pull/2229) support nonce for CSP
86+
* feat [#2229](https://github.com/gridstack/gridstack.js/pull/2229) support nonce for CSP. Thank you [@jedwards1211](https://github.com/jedwards1211)
87+
* feat: support nested grids with Angular component demo. Thank you R. Blanken for supporting this.
8788
* fix [#2206](https://github.com/gridstack/gridstack.js/issues/2206) `load()` with collision fix
8889
* fix [#2232](https://github.com/gridstack/gridstack.js/issues/2232) `autoPosition` bug loading from DOM
8990

src/gridstack.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,16 @@ export class GridStack {
137137

138138
// create the grid element, but check if the passed 'parent' already has grid styling and should be used instead
139139
let el = parent;
140-
if (!parent.classList.contains('grid-stack')) {
141-
let doc = document.implementation.createHTMLDocument(''); // IE needs a param
142-
doc.body.innerHTML = `<div class="grid-stack ${opt.class || ''}"></div>`;
143-
el = doc.body.children[0] as HTMLElement;
144-
parent.appendChild(el);
140+
const parentIsGrid = parent.classList.contains('grid-stack');
141+
if (!parentIsGrid || opt.addRemoveCB) {
142+
if (opt.addRemoveCB) {
143+
el = opt.addRemoveCB(parent, opt, true, true);
144+
} else {
145+
let doc = document.implementation.createHTMLDocument(''); // IE needs a param
146+
doc.body.innerHTML = `<div class="grid-stack ${opt.class || ''}"></div>`;
147+
el = doc.body.children[0] as HTMLElement;
148+
parent.appendChild(el);
149+
}
145150
}
146151

147152
// create grid class and load any children
@@ -182,7 +187,6 @@ export class GridStack {
182187
/** @internal true if we got created by drag over gesture, so we can removed on drag out (temporary) */
183188
public _isTemp?: boolean;
184189

185-
186190
/** @internal create placeholder DIV as needed */
187191
public get placeholder(): HTMLElement {
188192
if (!this._placeholder) {
@@ -409,7 +413,7 @@ export class GridStack {
409413
if (node?.el) {
410414
el = node.el; // re-use element stored in the node
411415
} else if (this.opts.addRemoveCB) {
412-
el = this.opts.addRemoveCB(this, options, true);
416+
el = this.opts.addRemoveCB(this.el, options, true, false);
413417
} else {
414418
let content = options?.content || '';
415419
let doc = document.implementation.createHTMLDocument(''); // IE needs a param
@@ -443,7 +447,7 @@ export class GridStack {
443447

444448
// see if there is a sub-grid to create
445449
if (node.subGrid) {
446-
this.makeSubGrid(node.el, undefined, undefined, false);
450+
this.makeSubGrid(node.el, undefined, undefined, false); //node.subGrid will be used as option in method, no need to pass
447451
}
448452

449453
// if we're adding an item into 1 column (_prevColumn is set only when going to 1) make sure
@@ -493,26 +497,29 @@ export class GridStack {
493497
}
494498

495499
// if we're converting an existing full item, move over the content to be the first sub item in the new grid
496-
// TODO: support this.opts.addRemoveCB for frameworks
497500
let content = node.el.querySelector('.grid-stack-item-content') as HTMLElement;
498501
let newItem: HTMLElement;
499502
let newItemOpt: GridStackNode;
500503
if (saveContent) {
501504
this._removeDD(node.el); // remove D&D since it's set on content div
502-
let doc = document.implementation.createHTMLDocument(''); // IE needs a param
503-
doc.body.innerHTML = `<div class="grid-stack-item"></div>`;
504-
newItem = doc.body.children[0] as HTMLElement;
505-
newItem.appendChild(content);
506505
newItemOpt = {...node, x:0, y:0};
507506
Utils.removeInternalForSave(newItemOpt);
508507
delete newItemOpt.subGrid;
509508
if (node.content) {
510509
newItemOpt.content = node.content;
511510
delete node.content;
512511
}
513-
doc.body.innerHTML = `<div class="grid-stack-item-content"></div>`;
514-
content = doc.body.children[0] as HTMLElement;
515-
node.el.appendChild(content);
512+
if (this.opts.addRemoveCB) {
513+
newItem = this.opts.addRemoveCB(this.el, newItemOpt, true, false);
514+
} else {
515+
let doc = document.implementation.createHTMLDocument(''); // IE needs a param
516+
doc.body.innerHTML = `<div class="grid-stack-item"></div>`;
517+
newItem = doc.body.children[0] as HTMLElement;
518+
newItem.appendChild(content);
519+
doc.body.innerHTML = `<div class="grid-stack-item-content"></div>`;
520+
content = doc.body.children[0] as HTMLElement;
521+
node.el.appendChild(content);
522+
}
516523
this._prepareDragDropByNode(node); // ... and restore original D&D
517524
}
518525

@@ -526,6 +533,9 @@ export class GridStack {
526533
setTimeout(() => style.transition = null); // recover animation
527534
}
528535

536+
if (this.opts.addRemoveCB) {
537+
ops.addRemoveCB = ops.addRemoveCB || this.opts.addRemoveCB;
538+
}
529539
let subGrid = node.subGrid = GridStack.addGrid(content, ops);
530540
if (nodeToAdd?._moving) subGrid._isTemp = true; // prevent re-nesting as we add over
531541
if (autoColumn) subGrid._autoColumn = true;
@@ -564,6 +574,7 @@ export class GridStack {
564574
pGrid.addWidget(n.el, n);
565575
});
566576
pGrid.batchUpdate(false);
577+
if (this.parentGridItem) delete this.parentGridItem.subGrid;
567578
delete this.parentGridItem;
568579

569580
// create an artificial event for the original grid now that this one is gone (got a leave, but won't get enter)
@@ -668,7 +679,7 @@ export class GridStack {
668679
let item = items.find(w => n.id === w.id);
669680
if (!item) {
670681
if (this.opts.addRemoveCB)
671-
this.opts.addRemoveCB(this, n, false);
682+
this.opts.addRemoveCB(this.el, n, false, false);
672683
removed.push(n); // batch keep track
673684
this.removeWidget(n.el, true, false);
674685
}
@@ -873,6 +884,7 @@ export class GridStack {
873884
}
874885
this._removeStylesheet();
875886
this.el.removeAttribute('gs-current-row');
887+
if (this.parentGridItem) delete this.parentGridItem.subGrid;
876888
delete this.parentGridItem;
877889
delete this.opts;
878890
delete this._placeholder;

0 commit comments

Comments
 (0)