diff --git a/README.md b/README.md
index c1e8e5387..89f25fd94 100644
--- a/README.md
+++ b/README.md
@@ -293,8 +293,8 @@ See [example](http://gridstack.github.io/gridstack.js/demo/mobile.html).
search for ['gridstack' under NPM](https://www.npmjs.com/search?q=gridstack&ranking=popularity) for latest, more to come...
-- **Angular**: see our Angular Demo. Working on exposing the Angular component wrapper we use internally, or use directive (TBD), or use ngFor example, etc... There are many way to do this.
-- **Angular9**: [lb-gridstack](https://github.com/pfms84/lb-gridstack) Note: very old v0.3 gridstack instance so recommend for **concept ONLY**.
+- **Angular**: we now ship out of the box with Angular wrapper components - see Angular Demo.
+- **Angular9**: [lb-gridstack](https://github.com/pfms84/lb-gridstack) Note: very old v0.3 gridstack instance so recommend for **concept ONLY if you wish to use directive instead**.
- **AngularJS**: [gridstack-angular](https://github.com/kdietrich/gridstack-angular)
- **Ember**: [ember-gridstack](https://github.com/yahoo/ember-gridstack)
- **knockout**: see [demo](https://gridstackjs.com/demo/knockout.html) using component, but check [custom bindings ticket](https://github.com/gridstack/gridstack.js/issues/465) which is likely better approach.
diff --git a/demo/angular/README.md b/demo/angular/README.md
index 112b3ddd8..66aae8deb 100644
--- a/demo/angular/README.md
+++ b/demo/angular/README.md
@@ -1,3 +1,6 @@
+
+see [**Angular wrapper doc**](./src/app/README.md) for actual usage. this is generic ng project info...
+
# Angular
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 12.2.13.
diff --git a/demo/angular/src/app/README.md b/demo/angular/src/app/README.md
new file mode 100644
index 000000000..43d91a763
--- /dev/null
+++ b/demo/angular/src/app/README.md
@@ -0,0 +1,52 @@
+# Angular wrapper
+
+The Angular [wrapper component](./gridstack.component.ts) is a better way to use Gridstack, but alternative raw [NgFor](./ngFor.ts) or [Simple](./simple.ts) demos are also given.
+
+## Usage
+
+Code
+
+```typescript
+import { GridStackOptions, GridStackWidget } from 'gridstack';
+import { GridstackComponent, nodesCB } from './gridstack.component';
+
+/** sample grid options and items to load... */
+public gridOptions: GridStackOptions = {
+ margin: 5,
+ float: true,
+}
+public items: GridStackWidget[] = [
+ {x:0, y:0, minW:2},
+ {x:1, y:1},
+ {x:2, y:2},
+];
+
+// called whenever items change size/position/etc..
+public onChange(data: nodesCB) {
+ console.log('change ', data.nodes.length > 1 ? data.nodes : data.nodes[0]);
+}
+
+// ngFor unique node id to have correct match between our items used and GS
+public identify(index: number, w: GridStackWidget) {
+ return w.id;
+}
+```
+HTML
+```angular2html
+
+
+ Hello
+
+
+```
+
+## Demo
+You can see a fuller example at [app.component](https://github.com/gridstack/gridstack.js/blob/master/demo/angular/src/app/app.component.ts).
+
+to build the demo, go to demo/angular and run `yarn` + `yarn start` and Navigate to `http://localhost:4200/`
+
+### Caveats
+
+ - This wrapper needs v7.1.2+ to run as it needs the latest changes
+ - This wrapper handles well ngFor loops, but if you're using a trackBy function (as I would recommend) and no element id change after an update, you must manually call the `Gridstack.update()` method directly.
+ - The original client list of items is not updated to match **content** changes made by gridstack (TBD later), but adding new item or removing (as shown in demo) will update those new items. Client could use change/added/removed events to sync that list if they wish to do so now.
diff --git a/demo/angular/src/app/app.component.html b/demo/angular/src/app/app.component.html
index 85fa0ae7b..8d3b18587 100644
--- a/demo/angular/src/app/app.component.html
+++ b/demo/angular/src/app/app.component.html
@@ -1,10 +1,10 @@
-
Pick a sample test case to load:
+
Pick a demo to load:
-
+
@@ -12,10 +12,17 @@
-
-
- HELLO
-
-
+
+
COMPONENT: Most complete example that uses Component wrapper for grid and gridItem
+
+
+
+
+
+
+ {{n.content}}
+
+
+
diff --git a/demo/angular/src/app/app.component.ts b/demo/angular/src/app/app.component.ts
index f7cb8cb23..872417ca3 100644
--- a/demo/angular/src/app/app.component.ts
+++ b/demo/angular/src/app/app.component.ts
@@ -1,7 +1,9 @@
import { Component } from '@angular/core';
-import { GridStackOptions } from 'gridstack';
-import { elementCB, nodesCB } from './gridstack.component';
+import { GridStackOptions, GridStackWidget } from 'gridstack';
+import { GridstackComponent, elementCB, nodesCB } from './gridstack.component';
+// unique ids sets for each item for correct ngFor updating
+let ids = 1;
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
@@ -9,18 +11,70 @@ import { elementCB, nodesCB } from './gridstack.component';
})
export class AppComponent {
// which sample to show
- show = 1;
+ show = 3;
- public gridstackConfig: GridStackOptions = {
+ /** sample grid options and items to load... */
+ public gridOptions: GridStackOptions = {
margin: 5,
float: true,
}
+ public items: GridStackWidget[] = [
+ {x: 0, y: 0, minW: 2},
+ {x: 1, y: 1},
+ {x: 2, y: 2},
+ ];
- public onChange(h: nodesCB) {
- console.log('change ', h.nodes.length > 1 ? h.nodes : h.nodes[0]);
+ constructor() {
+ // give them content and unique id to make sure we track them during changes below...
+ this.items.forEach(w => {
+ w.content = `item ${ids}`;
+ w.id = String(ids++);
+ })
}
- public onResizeStop(h: elementCB) {
- console.log('resizestop ', h.el.gridstackNode);
+ /** called whenever items change size/position/etc.. */
+ public onChange(data: nodesCB) {
+ console.log('change ', data.nodes.length > 1 ? data.nodes : data.nodes[0]);
+ // TODO: update our list to match ?
+ }
+
+ public onResizeStop(data: elementCB) {
+ console.log('resizestop ', data.el.gridstackNode);
+ }
+
+ /**
+ * CRUD TEST operations
+ */
+ public add(comp: GridstackComponent) {
+ // new array isn't required as Angular seem to detect changes to content
+ // this.items = [...this.items, { x:3, y:0, w:3, content:`item ${ids}`, id:String(ids++) }];
+ this.items.push({ x:3, y:0, w:3, content:`item ${ids}`, id:String(ids++)});
+ }
+
+ public delete(comp: GridstackComponent) {
+ this.items.pop();
+ }
+
+ public change(comp: GridstackComponent) {
+ // this will not update the DOM nor trigger gridstackItems.changes for GS to auto-update, so call GS update() instead
+ // this.items[0].w = 3;
+ // comp.updateAll();
+ const n = comp.grid?.engine.nodes[0];
+ if (n?.el) comp.grid?.update(n.el, {w:3});
+ }
+
+ /** test updating existing and creating new one */
+ public newLayout(comp: GridstackComponent) {
+ this.items = [
+ {x:0, y:1, id:'1', minW:1, w:1}, // new size/constrain
+ {x:1, y:1, id:'2'},
+ // {x:2, y:1, id:'3'}, // delete item
+ {x:3, y:0, w:3, content:'new item'}, // new item
+ ];
+ }
+
+ // ngFor unique node id to have correct match between our items used and GS
+ public identify(index: number, w: GridStackWidget) {
+ return w.id;
}
}
diff --git a/demo/angular/src/app/app.module.ts b/demo/angular/src/app/app.module.ts
index 819ae41dd..211c16094 100644
--- a/demo/angular/src/app/app.module.ts
+++ b/demo/angular/src/app/app.module.ts
@@ -10,10 +10,10 @@ import { AngularSimpleComponent } from './simple';
@NgModule({
declarations: [
- AppComponent,
- AngularSimpleComponent,
- AngularNgForTestComponent,
AngularNgForCmdTestComponent,
+ AngularNgForTestComponent,
+ AngularSimpleComponent,
+ AppComponent,
GridstackComponent,
GridstackItemComponent,
],
diff --git a/demo/angular/src/app/gridstack-item.component.ts b/demo/angular/src/app/gridstack-item.component.ts
index ce9637ef7..ba5fb99b2 100644
--- a/demo/angular/src/app/gridstack-item.component.ts
+++ b/demo/angular/src/app/gridstack-item.component.ts
@@ -1,5 +1,5 @@
-import {ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, Renderer2} from '@angular/core';
-import { GridItemHTMLElement, numberOrString } from 'gridstack';
+import { ChangeDetectionStrategy, Component, ElementRef, Input } from '@angular/core';
+import { GridItemHTMLElement, GridStackNode } from 'gridstack';
@Component({
selector: 'gridstack-item',
@@ -12,29 +12,40 @@ import { GridItemHTMLElement, numberOrString } from 'gridstack';
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class GridstackItemComponent implements OnInit {
+export class GridstackItemComponent {
- @Input() public x: number;
- @Input() public y: number;
- @Input() public w: number;
- @Input() public h: number;
- @Input() public minW: number;
- @Input() public minH: number;
- @Input() public maxW: number;
- @Input() public maxH: number;
- @Input() public id: numberOrString;
-
- constructor(
- private readonly _elementRef: ElementRef,
- private readonly renderer2: Renderer2,
- ) {
+ /** list of options for creating this item */
+ @Input() public set options(val: GridStackNode) {
+ val.el = this.element; // connect this element to options so we can convert to widget later
+ if (this.element.gridstackNode?.grid) {
+ this.element.gridstackNode.grid.update(this.element, val);
+ } else {
+ this._options = val; // store initial values (before we're built)
+ }
+ }
+ /** return the latest grid options (from GS once built, otherwise initial values) */
+ public get options(): GridStackNode {
+ return this.element.gridstackNode || this._options || {};
}
- get elementRef(): ElementRef {
- return this._elementRef;
+ private _options?: GridStackNode;
+
+ /** return the native element that contains grid specific fields as well */
+ public get element(): GridItemHTMLElement { return this.elementRef.nativeElement; }
+
+ /** clears the initial options now that we've built */
+ public clearOptions() {
+ delete this._options;
}
- public ngOnInit(): void {
- this.renderer2.addClass(this._elementRef.nativeElement, 'grid-stack-item');
+ constructor(private readonly elementRef: ElementRef) {
}
+
+ // none of those have parentElement set from which we could get the grid to auto-init ourself!
+ // so we will let the parent track us instead...
+ // ngOnInit() {
+ // this.element.parentElement
+ // }
+ // ngAfterContentInit() {
+ // }
}
diff --git a/demo/angular/src/app/gridstack.component.ts b/demo/angular/src/app/gridstack.component.ts
index 760a4bcac..aaae703f1 100644
--- a/demo/angular/src/app/gridstack.component.ts
+++ b/demo/angular/src/app/gridstack.component.ts
@@ -1,20 +1,8 @@
-import {
- ChangeDetectionStrategy,
- Component,
- ContentChildren,
- ElementRef,
- EventEmitter,
- Input,
- NgZone,
- OnDestroy,
- OnInit,
- Output,
- QueryList,
- Renderer2,
-} from '@angular/core';
-import { merge, Subject } from 'rxjs';
-import { map, takeUntil } from 'rxjs/operators';
-import { GridHTMLElement, GridItemHTMLElement, GridStack, GridStackNode, GridStackOptions } from 'gridstack';
+import { AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren, ElementRef, EventEmitter, Input,
+ NgZone, OnDestroy, OnInit, Output, QueryList } from '@angular/core';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { GridHTMLElement, GridItemHTMLElement, GridStack, GridStackNode, GridStackOptions, GridStackWidget } from 'gridstack';
import { GridstackItemComponent } from './gridstack-item.component';
@@ -35,14 +23,17 @@ export type droppedCB = {event: Event, previousNode: GridStackNode, newNode: Gri
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class GridstackComponent implements OnInit, OnDestroy {
+export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
/** track list of grid items so we can sync between DOM and GS internals */
- @ContentChildren(GridstackItemComponent) public gridstackItems: QueryList;
+ @ContentChildren(GridstackItemComponent) public gridstackItems?: QueryList;
- @Input() public options: GridStackOptions;
+ /** initial options for creation of the grid */
+ @Input() public set options(val: GridStackOptions) { this._options = val; }
+ /** return the current running options */
+ public get options(): GridStackOptions { return this._grid?.opts || this._options || {}; }
- /** individual list of all GridStackEvent callbacks handlers as output
+ /** individual list of GridStackEvent callbacks handlers as output
* otherwise use this.grid.on('name1 name2 name3', callback) to handle multiple at once
* see https://github.com/gridstack/gridstack.js/blob/master/demo/events.js#L4
*/
@@ -59,135 +50,111 @@ export class GridstackComponent implements OnInit, OnDestroy {
@Output() public resizestartCB = new EventEmitter();
@Output() public resizestopCB = new EventEmitter();
- public grid: GridStack;
- private readonly update$ = new Subject();
- private readonly destroy$ = new Subject();
+ /** return the native element that contains grid specific fields as well */
+ public get element(): GridHTMLElement { return this.elementRef.nativeElement; }
+
+ /** return the GridStack class */
+ public get grid(): GridStack | undefined { return this._grid; }
+
+ private _options?: GridStackOptions;
+ private _grid?: GridStack;
+ private ngUnsubscribe: Subject = new Subject();
constructor(
private readonly ngZone: NgZone,
private readonly elementRef: ElementRef,
- private readonly renderer2: Renderer2,
) {
}
public ngOnInit(): void {
- this.renderer2.addClass(this.elementRef.nativeElement, 'grid-stack');
+ // init ourself before the children are created since we track them below anyway - no need to double create+update widgets
+ this._grid = GridStack.init(this.options, this.element);
+ delete this._options; // GS has it now
}
- // wait until after DOM is ready to init gridstack - can't be ngOnInit() as angular ngFor or sub-components needs to run first!
+ /** wait until after all DOM is ready to init gridstack children (after angular ngFor and sub-components run first) */
public ngAfterContentInit(): void {
this.ngZone.runOutsideAngular(() => {
- // initialize the grid with given options
- this.grid = GridStack.init(this.options, this.elementRef.nativeElement);
+ // whenever the children list changes, re-update the layout
+ this.gridstackItems?.changes
+ .pipe(takeUntil(this.ngUnsubscribe))
+ .subscribe(() => this.updateAll());
+ // and do this once at least...
+ this.updateAll();
this.hookEvents(this.grid);
-
- merge(
- this.update$,
- this.gridstackItems.changes,
- ).pipe(
- map(() => this.gridstackItems.toArray()),
- takeUntil(this.destroy$),
- ).subscribe(items => {
- const gridItems = this.grid.getGridItems();
- let elementsToRemove = [...gridItems];
- this.grid.batchUpdate();
- // this.grid.column(this.options.column);
- for (const item of items) {
- const existingItem = gridItems.find(x => x.gridstackNode?.id === item.id);
- if (existingItem) {
- elementsToRemove = elementsToRemove.filter(x => x.gridstackNode?.id !== item.id);
- this.grid.update(existingItem, {
- h: item.h,
- w: item.w,
- x: item.x,
- y: item.y,
- });
- } else {
- this.grid.addWidget(item.elementRef.nativeElement, {
- id: item.id,
- h: item.h,
- w: item.w,
- x: item.x,
- y: item.y,
- minW: item.minW,
- minH: item.minH,
- });
- }
- }
- for (const gridItemHTMLElement of elementsToRemove) {
- this.grid.removeWidget(gridItemHTMLElement);
- }
- this.grid.commit();
- });
-
- this.update$.next();
});
}
public ngOnDestroy(): void {
- this.destroy$.next(true);
- this.destroy$.complete();
- this.update$.complete();
- this.grid.destroy();
+ this.ngUnsubscribe.next();
+ this.ngUnsubscribe.complete();
+ this.grid?.destroy();
+ delete this._grid;
}
- public update() {
- this.update$.next();
+ /**
+ * called when the list of items changes (or manually when you change content) - get a list of nodes and
+ * update the layout accordingly (which will take care of adding/removing items changed by Angular)
+ */
+ public updateAll() {
+ if (!this.grid) return;
+ const layout: GridStackWidget[] = [];
+ this.gridstackItems?.forEach(item => {
+ layout.push(item.options);
+ item.clearOptions();
+ });
+ this.grid.load(layout); // efficient that does diffs only
}
- private hookEvents(grid: GridStack) {
- grid.on('added', (event, nodes) => {
- this.ngZone.run(() => this.addedCB.emit({event, nodes: nodes as GridStackNode[]}));
+ /** get all known events as easy to use Outputs for convenience */
+ private hookEvents(grid?: GridStack) {
+ if (!grid) return;
+ grid.on('added', (event: Event, nodes: GridStackNode[]) => {
+ this.ngZone.run(() => this.addedCB.emit({event, nodes}));
});
- grid.on('change', (event, nodes) => {
- this.ngZone.run(() => this.changeCB.emit({event, nodes: nodes as GridStackNode[]}));
+ grid.on('change', (event: Event, nodes: GridStackNode[]) => {
+ this.ngZone.run(() => this.changeCB.emit({event, nodes}));
});
- grid.on('disable', (event) => {
+ grid.on('disable', (event: Event) => {
this.ngZone.run(() => this.disableCB.emit({event}));
});
- grid.on('drag', (event, el) => {
- this.ngZone.run(() => this.dragCB.emit({event, el: el as GridItemHTMLElement}));
+ grid.on('drag', (event: Event, el: GridItemHTMLElement) => {
+ this.ngZone.run(() => this.dragCB.emit({event, el}));
});
- grid.on('dragstart', (event, el) => {
- this.ngZone.run(() => this.dragstartCB.emit({event, el: el as GridItemHTMLElement}));
+ grid.on('dragstart', (event: Event, el: GridItemHTMLElement) => {
+ this.ngZone.run(() => this.dragstartCB.emit({event, el}));
});
- grid.on('dragstop', (event, el) => {
- this.ngZone.run(() => this.dragstopCB.emit({event, el: el as GridItemHTMLElement}));
+ grid.on('dragstop', (event: Event, el: GridItemHTMLElement) => {
+ this.ngZone.run(() => this.dragstopCB.emit({event, el}));
});
- grid.on('dropped', (event, previousNode, newNode) => {
- this.ngZone.run(() =>
- this.droppedCB.emit({
- event,
- previousNode: previousNode as GridStackNode,
- newNode: newNode as GridStackNode,
- })
- )
+ grid.on('dropped', (event: Event, previousNode: GridStackNode, newNode: GridStackNode) => {
+ this.ngZone.run(() => this.droppedCB.emit({event, previousNode, newNode}));
});
- grid.on('enable', (event) => {
+ grid.on('enable', (event: Event) => {
this.ngZone.run(() => this.enableCB.emit({event}));
});
- grid.on('removed', (event, nodes) => {
- this.ngZone.run(() => this.removedCB.emit({event, nodes: nodes as GridStackNode[]}));
+ grid.on('removed', (event: Event, nodes: GridStackNode[]) => {
+ this.ngZone.run(() => this.removedCB.emit({event, nodes}));
});
- grid.on('resize', (event, el) => {
- this.ngZone.run(() => this.resizeCB.emit({event, el: el as GridItemHTMLElement}));
+ grid.on('resize', (event: Event, el: GridItemHTMLElement) => {
+ this.ngZone.run(() => this.resizeCB.emit({event, el}));
});
- grid.on('resizestart', (event, el) => {
- this.ngZone.run(() => this.resizestartCB.emit({event, el: el as GridItemHTMLElement}));
+ grid.on('resizestart', (event: Event, el: GridItemHTMLElement) => {
+ this.ngZone.run(() => this.resizestartCB.emit({event, el}));
});
- grid.on('resizestop', (event, el) => {
- this.ngZone.run(() => this.resizestopCB.emit({event, el: el as GridItemHTMLElement}));
+ grid.on('resizestop', (event: Event, el: GridItemHTMLElement) => {
+ this.ngZone.run(() => this.resizestopCB.emit({event, el}));
});
}
}
diff --git a/demo/angular/src/app/ngFor.ts b/demo/angular/src/app/ngFor.ts
index 7c2b37a15..c1d0f13d2 100644
--- a/demo/angular/src/app/ngFor.ts
+++ b/demo/angular/src/app/ngFor.ts
@@ -11,13 +11,15 @@ let ids = 1;
@Component({
selector: "angular-ng-for-test",
template: `
-
Example using Angular ngFor to loop through items and create DOM items. This track changes made to the array of items, waits for DOM rendering, then update GS
+
ngFor: Example using Angular ngFor to loop through items and create DOM items. This track changes made to the array of items, waits for DOM rendering, then update GS
-
+
this.onChange(list as GridStackNode[]));
+ .on('change added', (event: Event, nodes: GridStackNode[]) => this.onChange(nodes));
// sync initial actual valued rendered (in case init() had to merge conflicts)
this.onChange();
@@ -97,8 +99,8 @@ export class AngularNgForTestComponent implements AfterViewInit {
*/
public add() {
// new array isn't required as Angular seem to detect changes to content
- // this.items = [...this.items, { x: 3, y: 0, w: 3, id: String(ids++) }];
- this.items.push({ x: 3, y: 0, w: 3, id: String(ids++) });
+ // this.items = [...this.items, { x:3, y:0, w:3, id:String(ids++) }];
+ this.items.push({ x:3, y:0, w:3, id:String(ids++) });
}
public delete() {
@@ -106,17 +108,19 @@ export class AngularNgForTestComponent implements AfterViewInit {
}
public change() {
- // this.items[0]?.w = 2; // this will not trigger gridstackItems.changes.subscribe, only DOM values are update, so call GS update() instead
+ // this will only update the DOM attr (from the ngFor loop in our template above)
+ // but not trigger gridstackItems.changes for GS to auto-update, so call GS update() instead
+ // this.items[0].w = 2;
const n = this.grid.engine.nodes[0];
- if (n) this.grid.update(n.el!, { w: 2 });
+ if (n?.el) this.grid.update(n.el, {w:3});
}
public newLayout() {
this.items = [ // test updating existing and creating new one
- {x: 0, y: 1, id: 1},
- {x: 1, y: 1, id: 2},
- // {x: 2, y: 1, id: 3}, // delete item
- {x: 3, y: 0, w: 3}, // new item
+ {x:0, y:1, id:'1'},
+ {x:1, y:1, id:'2'},
+ // {x:2, y:1, id:3}, // delete item
+ {x:3, y:0, w:3}, // new item
];
}
diff --git a/demo/angular/src/app/ngFor_cmd.ts b/demo/angular/src/app/ngFor_cmd.ts
index c3165b4ea..134e4db9e 100644
--- a/demo/angular/src/app/ngFor_cmd.ts
+++ b/demo/angular/src/app/ngFor_cmd.ts
@@ -11,7 +11,7 @@ import { GridItemHTMLElement, GridStack, GridStackWidget } from 'gridstack';
@Component({
selector: "angular-ng-for-cmd-test",
template: `
-
Example using Angular ngFor to loop through items, but uses an explicity command to let us update GS (see automatic better way)
+
ngFor CMD: Example using Angular ngFor to loop through items, but uses an explicity command to let us update GS (see automatic better way)