Skip to content
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 angular/projects/demo/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
<button (click)="saveGrid()">Save</button>
<button (click)="clearGrid()">Clear</button>
<button (click)="loadGrid()">Load</button>
<!-- add .grid-stack-item for acceptWidgets:true -->
<div class="sidebar-item grid-stack-item">Drag nested</div>
<div class="sidebar-item grid-stack-item">Comp N nested</div>

<!-- TODO: addGrid() in code for testing instead ? -->
<gridstack [options]="nestedGridOptions" (changeCB)="onChange($event)" (resizeStopCB)="onResizeStop($event)">
<div empty-content>Add items here or reload the grid</div>
Expand Down
36 changes: 22 additions & 14 deletions angular/projects/demo/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AngularSimpleComponent } from './simple';
import { AngularNgForTestComponent } from './ngFor';
import { AngularNgForCmdTestComponent } from './ngFor_cmd';

// NOTE: local testing of file
// TEST: local testing of file
// import { GridstackComponent, NgGridStackOptions, NgGridStackWidget, elementCB, gsCreateNgComponents, nodesCB } from './gridstack.component';
import { GridstackComponent, NgGridStackOptions, NgGridStackWidget, elementCB, gsCreateNgComponents, nodesCB } from 'gridstack/dist/angular';

Expand Down Expand Up @@ -46,12 +46,6 @@ export class AppComponent implements OnInit {
children: this.sub0,
}

// sidebar content to create storing the Widget description to be used on drop
public sidebarContent: NgGridStackWidget[] = [
{selector: 'app-a'},
{selector: 'app-b', w:2, maxW: 3},
];

// nested grid options
private subOptions: GridStackOptions = {
cellHeight: 50, // should be 50 - top/bottom
Expand All @@ -61,17 +55,20 @@ export class AppComponent implements OnInit {
};
public 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}];
public sub2: NgGridStackWidget[] = [ {x:0, y:0}, {x:0, y:1, w:2}];
public sub3: NgGridStackWidget = { selector: 'app-n', w:2, h:2, subGridOpts: { children: [{selector: 'app-a'}, {selector: 'app-b', y:0, x:1}]}};
private subChildren: NgGridStackWidget[] = [
{x:0, y:0, content: 'regular item'},
{x:1, y:0, w:4, h:4, subGridOpts: {children: this.sub1, class: 'sub1', ...this.subOptions}},
{x:5, y:0, w:3, h:4, subGridOpts: {children: this.sub2, class: 'sub2', ...this.subOptions}},
{x:1, y:0, w:4, h:4, subGridOpts: {children: this.sub1}},
// {x:5, y:0, w:3, h:4, subGridOpts: {children: this.sub2}},
this.sub3,
]
public nestedGridOptions: NgGridStackOptions = { // main grid options
cellHeight: 50,
margin: 5,
minRow: 2, // don't collapse when empty
acceptWidgets: true,
children: this.subChildren
subGridOpts: this.subOptions, // all sub grids will default to those
children: this.subChildren,
};
public twoGridOpt1: NgGridStackOptions = {
column: 6,
Expand All @@ -91,11 +88,20 @@ export class AppComponent implements OnInit {
public twoGridOpt2: NgGridStackOptions = { ...this.twoGridOpt1, float: false }
private serializedData?: NgGridStackOptions;

// sidebar content to create storing the Widget description to be used on drop
public sidebarContent6: NgGridStackWidget[] = [
{ w:2, h:2, subGridOpts: { children: [{content: 'nest 1'}, {content: 'nest 2'}]}},
this.sub3,
];
public sidebarContent7: NgGridStackWidget[] = [
{selector: 'app-a'},
{selector: 'app-b', w:2, maxW: 3},
];

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

Expand Down Expand Up @@ -132,9 +138,11 @@ export class AppComponent implements OnInit {
case 3: data = this.gridComp?.grid?.save(true, true); break;
case 4: data = this.items; break;
case 5: data = this.gridOptionsFull; break;
case 6: data = this.nestedGridOptions; break;
case 6: data = this.nestedGridOptions;
GridStack.setupDragIn('.sidebar-item', undefined, this.sidebarContent6);
break;
case 7: data = this.twoGridOpt1;
GridStack.setupDragIn('.sidebar>.grid-stack-item', undefined, this.sidebarContent);
GridStack.setupDragIn('.sidebar-item', undefined, this.sidebarContent7);
break;
}
if (this.origTextEl) this.origTextEl.nativeElement.value = JSON.stringify(data, null, ' ');
Expand Down
7 changes: 4 additions & 3 deletions angular/projects/demo/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { AppComponent } from './app.component';
import { AngularNgForTestComponent } from './ngFor';
import { AngularNgForCmdTestComponent } from './ngFor_cmd';
import { AngularSimpleComponent } from './simple';
import { AComponent, BComponent, CComponent } from './dummy.component';
import { AComponent, BComponent, CComponent, NComponent } from './dummy.component';

// local testing
// TEST local testing
// import { GridstackModule } from './gridstack.module';
// import { GridstackComponent } from './gridstack.component';
import { GridstackModule, GridstackComponent } from 'gridstack/dist/angular';
Expand All @@ -25,6 +25,7 @@ import { GridstackModule, GridstackComponent } from 'gridstack/dist/angular';
AComponent,
BComponent,
CComponent,
NComponent,
],
exports: [
],
Expand All @@ -34,6 +35,6 @@ import { GridstackModule, GridstackComponent } from 'gridstack/dist/angular';
export class AppModule {
constructor() {
// register all our dynamic components created in the grid
GridstackComponent.addComponentToSelectorType([AComponent, BComponent, CComponent]);
GridstackComponent.addComponentToSelectorType([AComponent, BComponent, CComponent, NComponent]);
}
}
23 changes: 21 additions & 2 deletions angular/projects/demo/src/app/dummy.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

// dummy testing component that will be grid items content

import { Component, OnDestroy, Input } from '@angular/core';
import { Component, OnDestroy, Input, ViewChild, ViewContainerRef } from '@angular/core';

// local testing
// TEST local testing
// import { BaseWidget } from './base-widget';
// import { NgCompInputs } from './gridstack.component';
import { BaseWidget, NgCompInputs } from 'gridstack/dist/angular';
Expand Down Expand Up @@ -37,3 +37,22 @@ export class BComponent extends BaseWidget implements OnDestroy {
export class CComponent extends BaseWidget implements OnDestroy {
ngOnDestroy() { console.log('Comp C destroyed'); }
}

/** Component that host a sub-grid as a child with controls above/below it. */
@Component({
selector: 'app-n',
template: `
<div>Comp N</div>
<ng-template #container></ng-template>
`,
/** make the subgrid take entire remaining space even when empty (so you can drag back inside without forcing 1 row) */
styles: [`
:host { height: 100%; display: flex; flex-direction: column; }
::ng-deep .grid-stack.grid-stack-nested { flex: 1; }
`],
})
export class NComponent extends BaseWidget implements OnDestroy {
/** this is where the dynamic nested grid will be hosted. gsCreateNgComponents() looks for 'container' like GridstackItemComponent */
@ViewChild('container', { read: ViewContainerRef, static: true}) public container?: ViewContainerRef;
ngOnDestroy() { console.log('Comp N destroyed'); }
}
3 changes: 2 additions & 1 deletion angular/projects/demo/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ h1 {
}
.grid-stack.grid-stack-nested {
background: none;
/* background-color: red; */
}
.grid-stack-item-content>.grid-stack.grid-stack-nested {
/* take entire space */
position: absolute;
inset: 0; /* TODO change top: if you have content in nested grid */
Expand Down
4 changes: 4 additions & 0 deletions angular/projects/lib/src/lib/base-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { NgCompInputs, NgGridStackWidget } from './gridstack.component';

@Injectable()
export abstract class BaseWidget {

/** variable that holds the complete definition of this widgets (with selector,x,y,w,h) */
public widgetItem?: NgGridStackWidget;

/**
* REDEFINE to return an object representing the data needed to re-create yourself, other than `selector` already handled.
* This should map directly to the @Input() fields of this objects on create, so a simple apply can be used on read
Expand Down
28 changes: 16 additions & 12 deletions angular/projects/lib/src/lib/gridstack.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type NgCompInputs = {[key: string]: any};
export interface NgGridStackWidget extends GridStackWidget {
selector?: string; // component type to create as content
input?: NgCompInputs; // serialized data for the component input fields
subGridOpts?: NgGridStackOptions; // nested grid options
}
export interface NgGridStackNode extends GridStackNode {
selector?: string; // component type to create as content
Expand Down Expand Up @@ -214,13 +215,17 @@ export function gsCreateNgComponents(host: GridCompHTMLElement | HTMLElement, w:
//
if (!host) return;
if (isGrid) {
const container = (host.parentElement as GridItemCompHTMLElement)?._gridItemComp?.container;
// TODO: figure out how to create ng component inside regular Div. need to access app injectors...
// if (!container) {
// const hostElement: Element = host;
// const environmentInjector: EnvironmentInjector;
// grid = createComponent(GridstackComponent, {environmentInjector, hostElement})?.instance;
// }

const gridItemCom = (host.parentElement as GridItemCompHTMLElement)?._gridItemComp;
if (!gridItemCom) return;
// check if gridItem has a child component with 'container' exposed to create under..
const container = (gridItemCom.childWidget as any)?.container || gridItemCom.container;
const gridRef = container?.createComponent(GridstackComponent);
const grid = gridRef?.instance;
if (!grid) return;
Expand All @@ -234,17 +239,16 @@ export function gsCreateNgComponents(host: GridCompHTMLElement | HTMLElement, w:
if (!gridItem) return;
gridItem.ref = gridItemRef

// 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
if (!w.subGridOpts) {
const selector = (w as NgGridStackWidget).selector;
const type = selector ? GridstackComponent.selectorToType[selector] : undefined;
if (type) {
const childWidget = gridItem.container?.createComponent(type)?.instance as BaseWidget;
// if proper BaseWidget subclass, save it and load additional data
if (childWidget && typeof childWidget.serialize === 'function' && typeof childWidget.deserialize === 'function') {
gridItem.childWidget = childWidget;
childWidget.deserialize(w);
}
// define what type of component to create as child, OR you can do it GridstackItemComponent template, but this is more generic
const selector = (w as NgGridStackWidget).selector;
const type = selector ? GridstackComponent.selectorToType[selector] : undefined;
if (type) {
const childWidget = gridItem.container?.createComponent(type)?.instance as BaseWidget;
// if proper BaseWidget subclass, save it and load additional data
if (childWidget && typeof childWidget.serialize === 'function' && typeof childWidget.deserialize === 'function') {
gridItem.childWidget = childWidget;
childWidget.widgetItem = w;
childWidget.deserialize(w);
}
}

Expand Down
1 change: 1 addition & 0 deletions doc/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ Change log
* fix: [#2736](https://github.com/gridstack/gridstack.js/bug/2736) safe practices around GridStackWidget.content no longer setting innerHTML
* fix: [#2231](https://github.com/gridstack/gridstack.js/bug/2231),[#1840](https://github.com/gridstack/gridstack.js/bug/1840),[#2354](https://github.com/gridstack/gridstack.js/bug/2354)
big overall to how we do sidepanel drag&drop helper. see release notes.
* feat: [#2818](https://github.com/gridstack/gridstack.js/pull/2818) support for Angular Component hosting true sub-grids (that size according to parent) without requring them to be only child of grid-item-content.

## 10.3.1 (2024-07-21)
* fix: [#2734](https://github.com/gridstack/gridstack.js/bug/2734) rotate() JS error
Expand Down
2 changes: 1 addition & 1 deletion src/dd-gridstack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export class DDGridStack {
dEl.setupDraggable({
...grid.opts.draggable,
...{
// containment: (grid.parentGridItem && grid.opts.dragOut === false) ? grid.el.parentElement : (grid.opts.draggable.containment || null),
// containment: (grid.parentGridNode && grid.opts.dragOut === false) ? grid.el.parentElement : (grid.opts.draggable.containment || null),
start: opts.start,
stop: opts.stop,
drag: opts.drag
Expand Down
46 changes: 23 additions & 23 deletions src/gridstack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ export class GridStack {
public engine: GridStackEngine;

/** point to a parent grid item if we're nested (inside a grid-item in between 2 Grids) */
public parentGridItem?: GridStackNode;
public parentGridNode?: GridStackNode;

protected static engineClass: typeof GridStackEngine;
protected resizeObserver: ResizeObserver;
Expand Down Expand Up @@ -357,13 +357,13 @@ export class GridStack {
}

// check if we're been nested, and if so update our style and keep pointer around (used during save)
const grandParent: GridItemHTMLElement = this.el.parentElement?.parentElement;
let parentGridItem = grandParent?.classList.contains(gridDefaults.itemClass) ? grandParent.gridstackNode : undefined;
if (parentGridItem) {
parentGridItem.subGrid = this;
this.parentGridItem = parentGridItem;
const parentGridItem: GridItemHTMLElement = this.el.closest('.' + gridDefaults.itemClass);
let parentNode = parentGridItem?.gridstackNode;
if (parentNode) {
parentNode.subGrid = this;
this.parentGridNode = parentNode;
this.el.classList.add('grid-stack-nested');
parentGridItem.el.classList.add('grid-stack-sub-grid');
parentNode.el.classList.add('grid-stack-sub-grid');
}

this._isAutoCellHeight = (opts.cellHeight === 'auto');
Expand Down Expand Up @@ -511,7 +511,7 @@ export class GridStack {
let grid: GridStack = this;
while (grid && !subGridTemplate) {
subGridTemplate = grid.opts?.subGridOpts;
grid = grid.parentGridItem?.grid;
grid = grid.parentGridNode?.grid;
}
//... and set the create options
ops = Utils.cloneDeep({ ...(subGridTemplate || {}), children: undefined, ...(ops || node.subGridOpts || {}) });
Expand Down Expand Up @@ -584,20 +584,20 @@ export class GridStack {
* to the original grid-item. Also called to remove empty sub-grids when last item is dragged out (since re-creating is simple)
*/
public removeAsSubGrid(nodeThatRemoved?: GridStackNode): void {
let pGrid = this.parentGridItem?.grid;
let pGrid = this.parentGridNode?.grid;
if (!pGrid) return;

pGrid.batchUpdate();
pGrid.removeWidget(this.parentGridItem.el, true, true);
pGrid.removeWidget(this.parentGridNode.el, true, true);
this.engine.nodes.forEach(n => {
// migrate any children over and offsetting by our location
n.x += this.parentGridItem.x;
n.y += this.parentGridItem.y;
n.x += this.parentGridNode.x;
n.y += this.parentGridNode.y;
pGrid.makeWidget(n.el, n);
});
pGrid.batchUpdate(false);
if (this.parentGridItem) delete this.parentGridItem.subGrid;
delete this.parentGridItem;
if (this.parentGridNode) delete this.parentGridNode.subGrid;
delete this.parentGridNode;

// create an artificial event for the original grid now that this one is gone (got a leave, but won't get enter)
if (nodeThatRemoved) {
Expand Down Expand Up @@ -979,8 +979,8 @@ export class GridStack {
this.el.parentNode.removeChild(this.el);
}
this._removeStylesheet();
if (this.parentGridItem) delete this.parentGridItem.subGrid;
delete this.parentGridItem;
if (this.parentGridNode) delete this.parentGridNode.subGrid;
delete this.parentGridNode;
delete this.opts;
delete this._placeholder;
delete this.engine;
Expand Down Expand Up @@ -1623,7 +1623,7 @@ export class GridStack {
/** @internal */
protected _updateContainerHeight(): GridStack {
if (!this.engine || this.engine.batchMode) return this;
const parent = this.parentGridItem;
const parent = this.parentGridNode;
let row = this.getRow() + this._extraDragRow; // this checks for minRow already
const cellHeight = this.opts.cellHeight as number;
const unit = this.opts.cellHeightUnit;
Expand Down Expand Up @@ -1776,9 +1776,9 @@ export class GridStack {

// see if we're nested and take our column count from our parent....
let columnChanged = false;
if (this._autoColumn && this.parentGridItem) {
if (this.opts.column !== this.parentGridItem.w) {
this.column(this.parentGridItem.w, 'none');
if (this._autoColumn && this.parentGridNode) {
if (this.opts.column !== this.parentGridNode.w) {
this.column(this.parentGridNode.w, 'none');
columnChanged = true;
}
} else {
Expand Down Expand Up @@ -1828,7 +1828,7 @@ export class GridStack {
protected _updateResizeEvent(forceRemove = false): GridStack {
// only add event if we're not nested (parent will call us) and we're auto sizing cells or supporting dynamic column (i.e. doing work)
// or supporting new sizeToContent option.
const trackSize = !this.parentGridItem && (this._isAutoCellHeight || this.opts.sizeToContent || this.opts.columnOpts
const trackSize = !this.parentGridNode && (this._isAutoCellHeight || this.opts.sizeToContent || this.opts.columnOpts
|| this.engine.nodes.find(n => n.sizeToContent));

if (!forceRemove && trackSize && !this.resizeObserver) {
Expand Down Expand Up @@ -2266,7 +2266,7 @@ export class GridStack {
oGrid.engine.removedNodes.push(origNode);
oGrid._triggerRemoveEvent()._triggerChangeEvent();
// if it's an empty sub-grid that got auto-created, nuke it
if (oGrid.parentGridItem && !oGrid.engine.nodes.length && oGrid.opts.subGridDynamic) {
if (oGrid.parentGridNode && !oGrid.engine.nodes.length && oGrid.opts.subGridDynamic) {
oGrid.removeAsSubGrid();
}
}
Expand Down Expand Up @@ -2303,7 +2303,7 @@ export class GridStack {
// resizeToContent is skipped in _prepareElement() until node is visible (clientHeight=0) so call it now
this.resizeToContentCheck(false, node);
if (subGrid) {
subGrid.parentGridItem = node;
subGrid.parentGridNode = node;
if (!subGrid.opts.styleInHead) subGrid._updateStyles(true); // re-create sub-grid styles now that we've moved
}
this._updateContainerHeight();
Expand Down