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)

diff --git a/demo/angular/src/app/simple.ts b/demo/angular/src/app/simple.ts index 79b2172f3..290ba6bfc 100644 --- a/demo/angular/src/app/simple.ts +++ b/demo/angular/src/app/simple.ts @@ -8,7 +8,7 @@ @Component({ selector: 'angular-simple-test', template: ` -

Simplest angular example using GridStack API directly, so not really using any angular construct per say other than waiting for DOM rendering

+

SIMPLEST: angular example using GridStack API directly, so not really using any angular construct per say other than waiting for DOM rendering