Skip to content

Commit

Permalink
feature: Angular nested grid
Browse files Browse the repository at this point in the history
* updated the Angular demo and wrapper to support nested grids
* fixed gridstack to call addRemoveCB for grids now, as well as sub-grid items
* WARNING: AddRemoveFcn had to change slightly to support calling grid or gridItem from same callback.
  • Loading branch information
adumesny committed Apr 2, 2023
1 parent 3b04280 commit 4d6d5cd
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 49 deletions.
4 changes: 4 additions & 0 deletions demo/angular/src/app/app.component.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.test-container {
margin-top: 30px;
}
button.active {
color: #fff;
background-color: #007bff;
}
26 changes: 19 additions & 7 deletions demo/angular/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@

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

<div class="test-container">
Expand Down Expand Up @@ -49,4 +50,15 @@
</gridstack>
</div>


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

</div>
29 changes: 26 additions & 3 deletions demo/angular/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,35 @@ export class AppComponent {
children: this.items,
}

// nested grid options
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}];
public sub2: GridStackWidget[] = [ {x:0, y:0}, {x:0, y:1, w:2}];
public subOptions: GridStackOptions = {
cellHeight: 50, // should be 50 - top/bottom
column: 'auto', // size to match container. make sure to include gridstack-extra.min.css
acceptWidgets: true, // will accept .grid-stack-item by default
margin: 5,
};
public nestedGridOptions: GridStackOptions = { // main grid options
cellHeight: 50,
margin: 5,
minRow: 2, // don't collapse when empty
disableOneColumnMode: true,
acceptWidgets: true,
id: 'main',
children: [
{x:0, y:0, content: 'regular item', id: 0},
{x:1, y:0, w:4, h:4, subGrid: {children: this.sub1, id:'sub1_grid', class: 'sub1', ...this.subOptions}},
{x:5, y:0, w:3, h:4, subGrid: {children: this.sub2, id:'sub2_grid', class: 'sub2', ...this.subOptions}},
]
};

constructor() {
// give them content and unique id to make sure we track them during changes below...
this.items.forEach(w => {
[...this.items, ...this.sub1, ...this.sub2].forEach(w => {
w.content = `item ${ids}`;
w.id = String(ids++);
})
});
}

/** called whenever items change size/position/etc.. */
Expand Down Expand Up @@ -72,7 +95,7 @@ export class AppComponent {
}

/**
* TEST TEMPLATE operations for ngFor case - NOT recommended unless you have no GS creating/re-parenting
* ngFor case: TEST TEMPLATE operations - NOT recommended unless you have no GS creating/re-parenting
*/
public addNgFor() {
// new array isn't required as Angular detects changes to content with trackBy:identify()
Expand Down
22 changes: 19 additions & 3 deletions demo/angular/src/app/gridstack-item.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,36 @@
* Copyright (c) 2022 Alain Dumesny - see GridStack root license
*/

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

/** store element to Ng Class pointer back */
export interface GridItemCompHTMLElement extends GridItemHTMLElement {
_gridItemComp?: GridstackItemComponent;
}

/**
* HTML Component Wrapper for gridstack items, in combination with GridstackComponent for parent grid
*/
@Component({
selector: 'gridstack-item',
template: `
<div class="grid-stack-item-content">
<!-- this is where you would create the right component based on some internal type or id. doing .content for demo purpose -->
{{options.content}}
<ng-content></ng-content>
<!-- where dynamic items go (like sub-grids) -->
<ng-template #container></ng-template>
</div>`,
styles: [`
:host { display: block; }
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GridstackItemComponent {
export class GridstackItemComponent implements OnDestroy {

/** container to append items dynamically */
@ViewChild('container', { read: ViewContainerRef, static: true}) public container?: ViewContainerRef;

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

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

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

constructor(private readonly elementRef: ElementRef<GridItemHTMLElement>) {
this.el._gridItemComp = this;
}

public ngOnDestroy(): void {
delete this.el._gridItemComp;
}
}
37 changes: 25 additions & 12 deletions demo/angular/src/app/gridstack.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@ import { AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren,
NgZone, OnDestroy, OnInit, Output, QueryList, ViewChild, ViewContainerRef } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { AddRemoveFcn, GridHTMLElement, GridItemHTMLElement, GridStack, GridStackNode, GridStackOptions, GridStackWidget } from 'gridstack';
import { GridHTMLElement, GridItemHTMLElement, GridStack, GridStackNode, GridStackOptions, GridStackWidget } from 'gridstack';

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

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

/** store element to Ng Class pointer back */
export interface GridCompHTMLElement extends GridHTMLElement {
_gridComp?: GridstackComponent;
}

/**
* HTML Component Wrapper for gridstack, in combination with GridstackItemComponent for the items
*/
Expand Down Expand Up @@ -71,28 +76,27 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
@Output() public resizeStopCB = new EventEmitter<elementCB>();

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

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

private _options?: GridStackOptions;
private _grid?: GridStack;
private loaded?: boolean;
private outsideAddRemove?: AddRemoveFcn;
private ngUnsubscribe: Subject<void> = new Subject();

constructor(
private readonly zone: NgZone,
private readonly elementRef: ElementRef<GridHTMLElement>,
private readonly elementRef: ElementRef<GridCompHTMLElement>,
) {
this.el._gridComp = this;
}

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

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

/**
Expand Down Expand Up @@ -159,14 +164,22 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
}

/** called by GS when a new item needs to be created, which we do as a Angular component, or deleted (skip) */
private _addRemoveCB(g: GridStack, w: GridStackWidget, add: boolean): HTMLElement | undefined {
private static _addRemoveCB(parent: GridCompHTMLElement | HTMLElement, w: GridStackWidget | GridStackOptions, add: boolean, isGrid: boolean): HTMLElement | undefined {
if (add) {
if (!this.container) return;
if (!parent) return;
// create the grid item dynamically - see https://angular.io/docs/ts/latest/cookbook/dynamic-component-loader.html
const gridItem = this.container.createComponent(GridstackItemComponent)?.instance;
return gridItem?.el;
if (isGrid) {
const gridItemComp = (parent.parentElement as GridItemCompHTMLElement)._gridItemComp;
const grid = gridItemComp?.container?.createComponent(GridstackComponent)?.instance;
if (grid) grid.options = w as GridStackOptions;
return grid?.el;
} else {
// TODO: use GridStackWidget to define what type of component to create as child, or do it in GridstackItemComponent template...
const gridComp = (parent as GridCompHTMLElement)._gridComp;
const gridItem = gridComp?.container?.createComponent(GridstackItemComponent)?.instance;
return gridItem?.el;
}
}
// if (this.outsideAddRemove) this.outsideAddRemove(g, w, add); // TODO: ?
return;
}
}
2 changes: 2 additions & 0 deletions demo/angular/src/styles.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/* re-use existing demo css file we already use for the plain demos - that include gridstack.css which is required */
@import "../../demo.css";
/* required file for gridstack 2-11 column */
@import "../../../dist/gridstack-extra.css";
3 changes: 2 additions & 1 deletion doc/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ Change log
<!-- END doctoc generated TOC please keep comment here to allow auto update -->

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

Expand Down
46 changes: 29 additions & 17 deletions src/gridstack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,16 @@ export class GridStack {

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

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


/** @internal create placeholder DIV as needed */
public get placeholder(): HTMLElement {
if (!this._placeholder) {
Expand Down Expand Up @@ -409,7 +413,7 @@ export class GridStack {
if (node?.el) {
el = node.el; // re-use element stored in the node
} else if (this.opts.addRemoveCB) {
el = this.opts.addRemoveCB(this, options, true);
el = this.opts.addRemoveCB(this.el, options, true, false);
} else {
let content = options?.content || '';
let doc = document.implementation.createHTMLDocument(''); // IE needs a param
Expand Down Expand Up @@ -443,7 +447,7 @@ export class GridStack {

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

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

// if we're converting an existing full item, move over the content to be the first sub item in the new grid
// TODO: support this.opts.addRemoveCB for frameworks
let content = node.el.querySelector('.grid-stack-item-content') as HTMLElement;
let newItem: HTMLElement;
let newItemOpt: GridStackNode;
if (saveContent) {
this._removeDD(node.el); // remove D&D since it's set on content div
let doc = document.implementation.createHTMLDocument(''); // IE needs a param
doc.body.innerHTML = `<div class="grid-stack-item"></div>`;
newItem = doc.body.children[0] as HTMLElement;
newItem.appendChild(content);
newItemOpt = {...node, x:0, y:0};
Utils.removeInternalForSave(newItemOpt);
delete newItemOpt.subGrid;
if (node.content) {
newItemOpt.content = node.content;
delete node.content;
}
doc.body.innerHTML = `<div class="grid-stack-item-content"></div>`;
content = doc.body.children[0] as HTMLElement;
node.el.appendChild(content);
if (this.opts.addRemoveCB) {
newItem = this.opts.addRemoveCB(this.el, newItemOpt, true, false);
} else {
let doc = document.implementation.createHTMLDocument(''); // IE needs a param
doc.body.innerHTML = `<div class="grid-stack-item"></div>`;
newItem = doc.body.children[0] as HTMLElement;
newItem.appendChild(content);
doc.body.innerHTML = `<div class="grid-stack-item-content"></div>`;
content = doc.body.children[0] as HTMLElement;
node.el.appendChild(content);
}
this._prepareDragDropByNode(node); // ... and restore original D&D
}

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

if (this.opts.addRemoveCB) {
ops.addRemoveCB = ops.addRemoveCB || this.opts.addRemoveCB;
}
let subGrid = node.subGrid = GridStack.addGrid(content, ops);
if (nodeToAdd?._moving) subGrid._isTemp = true; // prevent re-nesting as we add over
if (autoColumn) subGrid._autoColumn = true;
Expand Down Expand Up @@ -564,6 +574,7 @@ export class GridStack {
pGrid.addWidget(n.el, n);
});
pGrid.batchUpdate(false);
if (this.parentGridItem) delete this.parentGridItem.subGrid;
delete this.parentGridItem;

// create an artificial event for the original grid now that this one is gone (got a leave, but won't get enter)
Expand Down Expand Up @@ -668,7 +679,7 @@ export class GridStack {
let item = items.find(w => n.id === w.id);
if (!item) {
if (this.opts.addRemoveCB)
this.opts.addRemoveCB(this, n, false);
this.opts.addRemoveCB(this.el, n, false, false);
removed.push(n); // batch keep track
this.removeWidget(n.el, true, false);
}
Expand Down Expand Up @@ -873,6 +884,7 @@ export class GridStack {
}
this._removeStylesheet();
this.el.removeAttribute('gs-current-row');
if (this.parentGridItem) delete this.parentGridItem.subGrid;
delete this.parentGridItem;
delete this.opts;
delete this._placeholder;
Expand Down
Loading

0 comments on commit 4d6d5cd

Please sign in to comment.