Skip to content

Commit

Permalink
Merge pull request #2130 from adumesny/master
Browse files Browse the repository at this point in the history
Complete Angular wrapper
  • Loading branch information
adumesny committed Dec 29, 2022
2 parents 8f0b663 + 0da2115 commit 6afb383
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 159 deletions.
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -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 <a href="https://github.com/gridstack/gridstack.js/tree/master/demo/angular" target="_blank">Angular Demo</a>. 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 <a href="https://github.com/gridstack/gridstack.js/tree/master/demo/angular/src/app" target="_blank">Angular Demo</a>.
- **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.
Expand Down
3 changes: 3 additions & 0 deletions 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.
Expand Down
52 changes: 52 additions & 0 deletions demo/angular/src/app/README.md
@@ -0,0 +1,52 @@
# Angular wrapper

The Angular [wrapper component](./gridstack.component.ts) <gridstack> 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
<gridstack [options]="gridOptions" (changeCB)="onChange($event)">
<gridstack-item *ngFor="let n of items; trackBy: identify" [options]="n">
Hello
</gridstack-item>
</gridstack>
```

## 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.
21 changes: 14 additions & 7 deletions demo/angular/src/app/app.component.html
@@ -1,21 +1,28 @@

<div>
<p>Pick a sample test case to load:</p>
<p>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</button>
<button (click)="show=3">Component</button>
</div>

<div class="test-container">
<angular-simple-test *ngIf="show===0"></angular-simple-test>
<angular-ng-for-test *ngIf="show===1"></angular-ng-for-test>
<angular-ng-for-cmd-test *ngIf="show===2"></angular-ng-for-cmd-test>

<gridstack *ngIf="show===3" [options]="gridstackConfig" (changeCB)="onChange($event)" (resizestopCB)="onResizeStop($event)">
<gridstack-item [id]="'a'" [x]="0" [y]="0" [w]="6" [h]="4" [minH]="2" [minW]="3">
HELLO
</gridstack-item>
</gridstack>
<div *ngIf="show===3" >
<p><b>COMPONENT</b>: Most complete example that uses Component wrapper for grid and gridItem</p>
<button (click)="add(grid)">add item</button>
<button (click)="delete(grid)">remove item</button>
<button (click)="change(grid)">modify item</button>
<button (click)="newLayout(grid)">new layout</button>
<gridstack #grid [options]="gridOptions" (changeCB)="onChange($event)" (resizestopCB)="onResizeStop($event)">
<gridstack-item *ngFor="let n of items; trackBy: identify" [options]="n">
{{n.content}}
</gridstack-item>
</gridstack>
</div>

</div>
70 changes: 62 additions & 8 deletions demo/angular/src/app/app.component.ts
@@ -1,26 +1,80 @@
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',
styleUrls: ['./app.component.css']
})
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;
}
}
6 changes: 3 additions & 3 deletions demo/angular/src/app/app.module.ts
Expand Up @@ -10,10 +10,10 @@ import { AngularSimpleComponent } from './simple';

@NgModule({
declarations: [
AppComponent,
AngularSimpleComponent,
AngularNgForTestComponent,
AngularNgForCmdTestComponent,
AngularNgForTestComponent,
AngularSimpleComponent,
AppComponent,
GridstackComponent,
GridstackItemComponent,
],
Expand Down
53 changes: 32 additions & 21 deletions 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',
Expand All @@ -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<GridItemHTMLElement>,
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<GridItemHTMLElement> {
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<GridItemHTMLElement>) {
}

// 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() {
// }
}

0 comments on commit 6afb383

Please sign in to comment.