Skip to content

feature: Angular nested grid #2251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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